diff --git a/README.md b/README.md index 1a33e0c60..bc00c5c8d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ Build: [![Circle CI](https://circleci.com/gh/eclipse/hawkbit.svg?style=svg)](https://circleci.com/gh/eclipse/hawkbit) + + # Eclipse.IoT hawkBit - Update Server [hawkBit](https://projects.eclipse.org/projects/iot.hawkbit) is an domain independent back end solution for rolling out software updates to constrained edge devices as well as more powerful controllers and gateways connected to IP based networking infrastructure. diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/cache/CacheAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/cache/CacheAutoConfiguration.java index 6696c44ca..a170dfcd3 100644 --- a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/cache/CacheAutoConfiguration.java +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/cache/CacheAutoConfiguration.java @@ -25,6 +25,7 @@ import org.springframework.cache.interceptor.CacheOperationInvocationContext; import org.springframework.cache.interceptor.SimpleCacheResolver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; /** * A configuration for configuring the spring {@link CacheManager} for specific @@ -33,9 +34,6 @@ import org.springframework.context.annotation.Configuration; * * This is done by providing a special {@link TenantCacheResolver} which * generates a cache name included the current tenant. - * - * - * */ @Configuration @EnableCaching @@ -51,18 +49,27 @@ public class CacheAutoConfiguration extends CachingConfigurerSupport { @Override @Bean @ConditionalOnMissingBean + @Primary public TenancyCacheManager cacheManager() { - return new TenantAwareCacheManager(new GuavaCacheManager(), tenantAware); + return new TenantAwareCacheManager(directCacheManager(), tenantAware); + } + + /** + * @return the direct cache manager to access without tenant aware check, + * cause in sometimes it's necessary to access the cache directly + * without having the current tenant, e.g. initial creation of + * tenant + */ + @Bean(name = "directCacheManager") + @ConditionalOnMissingBean(name = "directCacheManager") + public CacheManager directCacheManager() { + return new GuavaCacheManager(); } /** * A {@link SimpleCacheResolver} implementation which includes the * {@link TenantAware#getCurrentTenant()} into the cache name before * resolving it. - * - * - * - * */ public class TenantCacheResolver extends SimpleCacheResolver { diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/cache/DownloadIdCacheAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/cache/DownloadIdCacheAutoConfiguration.java index 495200dec..134b50d90 100644 --- a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/cache/DownloadIdCacheAutoConfiguration.java +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/cache/DownloadIdCacheAutoConfiguration.java @@ -31,7 +31,7 @@ public class DownloadIdCacheAutoConfiguration { private CacheManager cacheManager; /** - * Bean for the downlod id cache. + * Bean for the download id cache. * * @return the cache */ diff --git a/hawkbit-cache-redis/pom.xml b/hawkbit-cache-redis/pom.xml index 09567291b..637a6b49f 100644 --- a/hawkbit-cache-redis/pom.xml +++ b/hawkbit-cache-redis/pom.xml @@ -45,6 +45,12 @@ + + org.eclipse.hawkbit + hawkbit-repository-api + ${project.version} + test + org.springframework.boot spring-boot-starter-test diff --git a/hawkbit-cache-redis/src/main/java/org/eclipse/hawkbit/cache/RedisConfiguration.java b/hawkbit-cache-redis/src/main/java/org/eclipse/hawkbit/cache/RedisConfiguration.java index edc183b17..fc3364d81 100644 --- a/hawkbit-cache-redis/src/main/java/org/eclipse/hawkbit/cache/RedisConfiguration.java +++ b/hawkbit-cache-redis/src/main/java/org/eclipse/hawkbit/cache/RedisConfiguration.java @@ -15,6 +15,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; @@ -50,8 +51,14 @@ public class RedisConfiguration { * @return the spring redis cache manager. */ @Bean + @Primary public CacheManager cacheManager() { - return new TenantAwareCacheManager(new RedisCacheManager(redisTemplate()), tenantAware); + return new TenantAwareCacheManager(directCacheManager(), tenantAware); + } + + @Bean(name = "directCacheManager") + public CacheManager directCacheManager() { + return new RedisCacheManager(redisTemplate()); } /** diff --git a/hawkbit-cache-redis/src/test/java/org/eclipse/hawkbit/cache/eventbus/EventDistributorTest.java b/hawkbit-cache-redis/src/test/java/org/eclipse/hawkbit/cache/eventbus/EventDistributorTest.java index c1ff54961..bebd82484 100644 --- a/hawkbit-cache-redis/src/test/java/org/eclipse/hawkbit/cache/eventbus/EventDistributorTest.java +++ b/hawkbit-cache-redis/src/test/java/org/eclipse/hawkbit/cache/eventbus/EventDistributorTest.java @@ -16,8 +16,8 @@ import static org.mockito.Mockito.verify; import java.util.Collection; -import org.eclipse.hawkbit.eventbus.event.DownloadProgressEvent; import org.eclipse.hawkbit.eventbus.event.EntityEvent; +import org.eclipse.hawkbit.repository.eventbus.event.DownloadProgressEvent; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -56,7 +56,7 @@ public class EventDistributorTest { @Test public void distributeDistributedEventSendsToRedis() { - final DownloadProgressEvent event = new DownloadProgressEvent("tenant", 123L, 10); + final DownloadProgressEvent event = new DownloadProgressEvent("tenant", 123L, 500L, 100L, 200L); underTest.distribute(event); // origin node ID should be set by distributing the event @@ -67,7 +67,7 @@ public class EventDistributorTest { @Test public void dontDistributeDistributedEventIfSameNode() { final String knownNodeId = EventDistributor.getNodeId(); - final DownloadProgressEvent event = new DownloadProgressEvent("tenant", 123L, 10); + final DownloadProgressEvent event = new DownloadProgressEvent("tenant", 123L, 500L, 100L, 200L); event.setNodeId(knownNodeId); // test @@ -79,7 +79,7 @@ public class EventDistributorTest { @Test public void handleDistributedMessageFromRedis() { - final DownloadProgressEvent event = new DownloadProgressEvent("tenant", 123L, 10); + final DownloadProgressEvent event = new DownloadProgressEvent("tenant", 123L, 500L, 100L, 200L); final String knownChannel = "someChannel"; underTest.handleMessage(event, knownChannel); @@ -90,7 +90,7 @@ public class EventDistributorTest { @Test public void handleDistributedMessageFilteredIfSameNodeId() { - final DownloadProgressEvent event = new DownloadProgressEvent("tenant", 123L, 10); + final DownloadProgressEvent event = new DownloadProgressEvent("tenant", 123L, 500L, 100L, 200L); final String knownChannel = "someChannel"; event.setOriginNodeId(EventDistributor.getNodeId()); diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/DownloadProgressEvent.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/DownloadProgressEvent.java deleted file mode 100644 index 74679f02e..000000000 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/eventbus/event/DownloadProgressEvent.java +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) 2015 Bosch Software Innovations GmbH and others. - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html - */ -package org.eclipse.hawkbit.eventbus.event; - -/** - * Event that contains an updated download progress for a given Action. - * - * - * - * - */ -public class DownloadProgressEvent extends AbstractDistributedEvent { - - private static final long serialVersionUID = 1L; - - private final Long statusId; - private final int progressPercent; - - /** - * Constructor. - * - * @param tenant - * the tenant for this event - * @param statusId - * of {@link UpdateActionStatus} - * @param progressPercent - * number (1-100) - */ - public DownloadProgressEvent(final String tenant, final Long statusId, final int progressPercent) { - // the revision of the DownloadProgressEvent is just equal the - // progressPercentage due the - // percentage is going from 0 to 100. - super(statusId, tenant); - this.statusId = statusId; - this.progressPercent = progressPercent; - } - - /** - * @return the statusId - */ - public Long getStatusId() { - return statusId; - } - - /** - * @return the progressPercent - */ - public int getProgressPercent() { - return progressPercent; - } -} diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetFields.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetFields.java index 0a696fdb6..472c161ff 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetFields.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetFields.java @@ -64,7 +64,12 @@ public enum TargetFields implements FieldNameProvider { /** * The tags field. */ - TAG("tags.name"); + TAG("tags.name"), + + /** + * Last time the target or DMF client polled. + */ + LASTCONTROLLERREQUESTAT("targetInfo.lastTargetQuery"); private final String fieldName; private List subEntityAttribues; diff --git a/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiArtifactStoreController.java b/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiArtifactStoreController.java index 75d502c47..e2af0a873 100644 --- a/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiArtifactStoreController.java +++ b/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiArtifactStoreController.java @@ -93,12 +93,12 @@ public class DdiArtifactStoreController implements DdiDlArtifactStoreControllerR // we set a download status only if we are aware of the // targetid, i.e. authenticated and not anonymous if (targetid != null && !"anonymous".equals(targetid)) { - final Action action = checkAndReportDownloadByTarget( + final ActionStatus actionStatus = checkAndReportDownloadByTarget( requestResponseContextHolder.getHttpServletRequest(), targetid, artifact); result = RestResourceConversionHelper.writeFileResponse(artifact, requestResponseContextHolder.getHttpServletResponse(), requestResponseContextHolder.getHttpServletRequest(), file, controllerManagement, - action.getId()); + actionStatus.getId()); } else { result = RestResourceConversionHelper.writeFileResponse(artifact, requestResponseContextHolder.getHttpServletResponse(), @@ -131,7 +131,7 @@ public class DdiArtifactStoreController implements DdiDlArtifactStoreControllerR return new ResponseEntity<>(HttpStatus.OK); } - private Action checkAndReportDownloadByTarget(final HttpServletRequest request, final String targetid, + private ActionStatus checkAndReportDownloadByTarget(final HttpServletRequest request, final String targetid, final LocalArtifact artifact) { final Target target = controllerManagement.updateLastTargetQuery(targetid, IpUtil.getClientIpFromRequest(request, securityProperties)); @@ -152,8 +152,8 @@ public class DdiArtifactStoreController implements DdiDlArtifactStoreControllerR actionStatus.addMessage( RepositoryConstants.SERVER_MESSAGE_PREFIX + "Target downloads: " + request.getRequestURI()); } - controllerManagement.addInformationalActionStatus(actionStatus); - return action; + + return controllerManagement.addInformationalActionStatus(actionStatus); } } diff --git a/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java b/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java index 120e6c246..9dd65d5fc 100644 --- a/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java +++ b/hawkbit-ddi-resource/src/main/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootController.java @@ -156,8 +156,8 @@ public class DdiRootController implements DdiRootControllerRestApi { if (ifMatch != null && !RestResourceConversionHelper.matchesHttpHeader(ifMatch, artifact.getSha1Hash())) { result = new ResponseEntity<>(HttpStatus.PRECONDITION_FAILED); } else { - final Action action = checkAndLogDownload(requestResponseContextHolder.getHttpServletRequest(), target, - module); + final ActionStatus action = checkAndLogDownload(requestResponseContextHolder.getHttpServletRequest(), + target, module); result = RestResourceConversionHelper.writeFileResponse(artifact, requestResponseContextHolder.getHttpServletResponse(), requestResponseContextHolder.getHttpServletRequest(), file, controllerManagement, @@ -167,7 +167,7 @@ public class DdiRootController implements DdiRootControllerRestApi { return result; } - private Action checkAndLogDownload(final HttpServletRequest request, final Target target, + private ActionStatus checkAndLogDownload(final HttpServletRequest request, final Target target, final SoftwareModule module) { final Action action = controllerManagement .getActionForDownloadByTargetAndSoftwareModule(target.getControllerId(), module); @@ -185,8 +185,8 @@ public class DdiRootController implements DdiRootControllerRestApi { statusMessage.addMessage( RepositoryConstants.SERVER_MESSAGE_PREFIX + "Target downloads " + request.getRequestURI()); } - controllerManagement.addInformationalActionStatus(statusMessage); - return action; + + return controllerManagement.addInformationalActionStatus(statusMessage); } private static boolean checkModule(final String fileName, final SoftwareModule module) { diff --git a/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiArtifactDownloadTest.java b/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiArtifactDownloadTest.java index d8931ab41..c618fa667 100644 --- a/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiArtifactDownloadTest.java +++ b/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiArtifactDownloadTest.java @@ -29,7 +29,7 @@ import java.util.List; import java.util.TimeZone; import org.apache.commons.lang3.RandomUtils; -import org.eclipse.hawkbit.eventbus.event.DownloadProgressEvent; +import org.eclipse.hawkbit.repository.eventbus.event.DownloadProgressEvent; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.Artifact; @@ -63,11 +63,15 @@ import ru.yandex.qatools.allure.annotations.Stories; @Stories("Artifact Download Resource") public class DdiArtifactDownloadTest extends AbstractRestIntegrationTestWithMongoDB { + private static final int ARTIFACT_SIZE = 5 * 1024 * 1024; + public DdiArtifactDownloadTest() { LOG = LoggerFactory.getLogger(DdiArtifactDownloadTest.class); } private volatile int downLoadProgress = 0; + private volatile long shippedBytes = 0; + private volatile long shippedBytesTotal = 0; private final SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz"); @@ -247,6 +251,8 @@ public class DdiArtifactDownloadTest extends AbstractRestIntegrationTestWithMong @Description("Tests valid downloads through the artifact resource by identifying the artifact not by ID but file name.") public void downloadArtifactThroughFileName() throws Exception { downLoadProgress = 1; + shippedBytes = 0; + shippedBytesTotal = 0; eventBus.register(this); assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); @@ -260,7 +266,7 @@ public class DdiArtifactDownloadTest extends AbstractRestIntegrationTestWithMong final DistributionSet ds = testdataFactory.createDistributionSet(""); // create artifact - final byte random[] = RandomUtils.nextBytes(5 * 1024 * 1024); + final byte random[] = RandomUtils.nextBytes(ARTIFACT_SIZE); final LocalArtifact artifact = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), ds.findFirstModuleByType(osType).getId(), "file1", false); @@ -287,6 +293,7 @@ public class DdiArtifactDownloadTest extends AbstractRestIntegrationTestWithMong // download complete assertThat(downLoadProgress).isEqualTo(10); + assertThat(shippedBytes).isEqualTo(shippedBytesTotal).isEqualTo(ARTIFACT_SIZE); } @Test @@ -324,35 +331,8 @@ public class DdiArtifactDownloadTest extends AbstractRestIntegrationTestWithMong + "anonymous as authorization is notpossible, e.g. chekc if the controller has the artifact assigned.") public void downloadArtifactByNameFailsIfNotAuthenticated() throws Exception { downLoadProgress = 1; - eventBus.register(this); - - assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); - - // create target - Target target = entityFactory.generateTarget("4712"); - target = targetManagement.createTarget(target); - final List targets = new ArrayList(); - targets.add(target); - - // create ds - final DistributionSet ds = testdataFactory.createDistributionSet(""); - - // create artifact - final byte random[] = RandomUtils.nextBytes(5 * 1024); - final Artifact artifact = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), - ds.findFirstModuleByType(osType).getId(), "file1.tar.bz2", false); - - // download fails as artifact is not yet assigned to target - deploymentManagement.assignDistributionSet(ds, targets); - mvc.perform(get("/controller/artifacts/v1/filename/{filename}", "file1.tar.bz2")) - .andExpect(status().isNotFound()); - } - - @Test - @WithUser(principal = "4712", authorities = "ROLE_CONTROLLER", allSpPermissions = true) - @Description("Ensures that an authenticated and named controller is permitted to download.") - public void downloadArtifactByNameByNamedController() throws Exception { - downLoadProgress = 1; + shippedBytes = 0; + shippedBytesTotal = 0; eventBus.register(this); assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); @@ -367,7 +347,41 @@ public class DdiArtifactDownloadTest extends AbstractRestIntegrationTestWithMong final DistributionSet ds = testdataFactory.createDistributionSet(""); // create artifact - final byte random[] = RandomUtils.nextBytes(5 * 1024 * 1024); + final byte random[] = RandomUtils.nextBytes(ARTIFACT_SIZE); + artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), + ds.findFirstModuleByType(osType).getId(), "file1.tar.bz2", false); + + // download fails as artifact is not yet assigned to target + deploymentManagement.assignDistributionSet(ds, targets); + mvc.perform(get("/controller/artifacts/v1/filename/{filename}", "file1.tar.bz2")) + .andExpect(status().isNotFound()); + + assertThat(downLoadProgress).isEqualTo(1); + assertThat(shippedBytes).isEqualTo(shippedBytesTotal).isEqualTo(0L); + } + + @Test + @WithUser(principal = "4712", authorities = "ROLE_CONTROLLER", allSpPermissions = true) + @Description("Ensures that an authenticated and named controller is permitted to download.") + public void downloadArtifactByNameByNamedController() throws Exception { + downLoadProgress = 1; + shippedBytes = 0; + shippedBytesTotal = 0; + eventBus.register(this); + + assertThat(softwareManagement.findSoftwareModulesAll(pageReq)).hasSize(0); + + // create target + Target target = entityFactory.generateTarget("4712"); + target = targetManagement.createTarget(target); + final List targets = new ArrayList<>(); + targets.add(target); + + // create ds + final DistributionSet ds = testdataFactory.createDistributionSet(""); + + // create artifact + final byte random[] = RandomUtils.nextBytes(ARTIFACT_SIZE); final Artifact artifact = artifactManagement.createLocalArtifact(new ByteArrayInputStream(random), ds.findFirstModuleByType(osType).getId(), "file1", false); @@ -400,6 +414,7 @@ public class DdiArtifactDownloadTest extends AbstractRestIntegrationTestWithMong // download complete assertThat(downLoadProgress).isEqualTo(10); + assertThat(shippedBytes).isEqualTo(shippedBytesTotal).isEqualTo(ARTIFACT_SIZE); } @Test @@ -561,5 +576,8 @@ public class DdiArtifactDownloadTest extends AbstractRestIntegrationTestWithMong @Subscribe public void listen(final DownloadProgressEvent event) { downLoadProgress++; + shippedBytes += event.getShippedBytesSinceLast(); + shippedBytesTotal = event.getShippedBytesOverall(); + } } diff --git a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/systemmanagement/MgmtSystemTenantServiceUsage.java b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/systemmanagement/MgmtSystemTenantServiceUsage.java index 8cce4314b..c3e37421d 100644 --- a/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/systemmanagement/MgmtSystemTenantServiceUsage.java +++ b/hawkbit-mgmt-api/src/main/java/org/eclipse/hawkbit/mgmt/json/model/systemmanagement/MgmtSystemTenantServiceUsage.java @@ -32,7 +32,6 @@ public class MgmtSystemTenantServiceUsage { * @param tenantName */ public MgmtSystemTenantServiceUsage(final String tenantName) { - super(); this.tenantName = tenantName; } diff --git a/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java b/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java index 3cb9222aa..28ff3d9b9 100644 --- a/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java +++ b/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetResourceTest.java @@ -8,12 +8,14 @@ */ package org.eclipse.hawkbit.mgmt.rest.resource; +import static com.google.common.collect.Lists.newArrayList; import static org.fest.assertions.api.Assertions.assertThat; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.notNullValue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -33,7 +35,6 @@ import org.eclipse.hawkbit.exception.SpServerError; import org.eclipse.hawkbit.im.authentication.SpPermission; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.repository.ActionFields; -import org.eclipse.hawkbit.repository.ActionStatusFields; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.jpa.model.JpaTargetInfo; @@ -90,6 +91,7 @@ public class MgmtTargetResourceTest extends AbstractRestIntegrationTest { private static final String JSON_PATH_FIELD_CONTENT = ".content"; private static final String JSON_PATH_FIELD_SIZE = ".size"; private static final String JSON_PATH_FIELD_TOTAL = ".total"; + private static final String JSON_PATH_FIELD_LAST_REQUEST_AT = ".lastControllerRequestAt"; // target // $.field @@ -101,6 +103,7 @@ public class MgmtTargetResourceTest extends AbstractRestIntegrationTest { private static final String JSON_PATH_ID = JSON_PATH_ROOT + JSON_PATH_FIELD_ID; private static final String JSON_PATH_CONTROLLERID = JSON_PATH_ROOT + JSON_PATH_FIELD_CONTROLLERID; private static final String JSON_PATH_DESCRIPTION = JSON_PATH_ROOT + JSON_PATH_FIELD_DESCRIPTION; + private static final String JSON_PATH_LAST_REQUEST_AT = JSON_PATH_ROOT + JSON_PATH_FIELD_LAST_REQUEST_AT; @Test @Description("Ensures that actions list is in exptected order.") @@ -425,6 +428,7 @@ public class MgmtTargetResourceTest extends AbstractRestIntegrationTest { .andExpect(jsonPath("$content.[?(@.name==" + idA + ")][0].controllerId", equalTo(idA))) .andExpect(jsonPath("$content.[?(@.name==" + idA + ")][0].createdBy", equalTo("bumlux"))) .andExpect(jsonPath("$content.[?(@.name==" + idA + ")][0].updateStatus", equalTo("unknown"))) + .andExpect(jsonPath("$content.[?(@.name==" + idA + ")][0].lastControllerRequestAt", notNullValue())) // idB .andExpect(jsonPath("$content.[?(@.name==" + idB + ")][0]._links.self.href", equalTo(linksHrefPrefix + idB))) @@ -433,6 +437,7 @@ public class MgmtTargetResourceTest extends AbstractRestIntegrationTest { .andExpect(jsonPath("$content.[?(@.name==" + idB + ")][0].controllerId", equalTo(idB))) .andExpect(jsonPath("$content.[?(@.name==" + idB + ")][0].createdBy", equalTo("bumlux"))) .andExpect(jsonPath("$content.[?(@.name==" + idB + ")][0].updateStatus", equalTo("unknown"))) + .andExpect(jsonPath("$content.[?(@.name==" + idA + ")][0].lastControllerRequestAt", notNullValue())) // idC .andExpect(jsonPath("$content.[?(@.name==" + idC + ")][0]._links.self.href", equalTo(linksHrefPrefix + idC))) @@ -440,7 +445,8 @@ public class MgmtTargetResourceTest extends AbstractRestIntegrationTest { .andExpect(jsonPath("$content.[?(@.name==" + idC + ")][0].description", equalTo(idC))) .andExpect(jsonPath("$content.[?(@.name==" + idC + ")][0].controllerId", equalTo(idC))) .andExpect(jsonPath("$content.[?(@.name==" + idC + ")][0].createdBy", equalTo("bumlux"))) - .andExpect(jsonPath("$content.[?(@.name==" + idC + ")][0].updateStatus", equalTo("unknown"))); + .andExpect(jsonPath("$content.[?(@.name==" + idC + ")][0].updateStatus", equalTo("unknown"))) + .andExpect(jsonPath("$content.[?(@.name==" + idA + ")][0].lastControllerRequestAt", notNullValue())); } @Test @@ -518,7 +524,7 @@ public class MgmtTargetResourceTest extends AbstractRestIntegrationTest { // create first a target which can be retrieved by rest interface final String knownControllerId = "1"; final String knownName = "someName"; - createSingleTarget(knownControllerId, knownName); + final Target target = createSingleTarget(knownControllerId, knownName); final String hrefPrefix = "http://localhost/rest/v1/targets/" + knownControllerId + "/"; // test mvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownControllerId)) @@ -526,6 +532,7 @@ public class MgmtTargetResourceTest extends AbstractRestIntegrationTest { .andExpect(jsonPath(JSON_PATH_NAME, equalTo(knownName))) .andExpect(jsonPath(JSON_PATH_CONTROLLERID, equalTo(knownControllerId))) .andExpect(jsonPath(JSON_PATH_DESCRIPTION, equalTo(TARGET_DESCRIPTION_TEST))) + .andExpect(jsonPath(JSON_PATH_LAST_REQUEST_AT, equalTo(target.getTargetInfo().getLastTargetQuery()))) .andExpect(jsonPath("$.pollStatus", hasKey("lastRequestAt"))) .andExpect(jsonPath("$.pollStatus", hasKey("nextExpectedRequestAt"))) .andExpect(jsonPath("$.pollStatus.overdue", equalTo(false))) @@ -1075,8 +1082,6 @@ public class MgmtTargetResourceTest extends AbstractRestIntegrationTest { private List generateTargetWithTwoUpdatesWithOneOverride(final String knownTargetId) throws InterruptedException { - final PageRequest pageRequest = new PageRequest(0, 100, Direction.ASC, ActionStatusFields.ID.getFieldName()); - Target target = entityFactory.generateTarget(knownTargetId); target = targetManagement.createTarget(target); final List targets = new ArrayList<>(); @@ -1122,7 +1127,7 @@ public class MgmtTargetResourceTest extends AbstractRestIntegrationTest { @Test public void assignDistributionSetToTarget() throws Exception { - final Target target = targetManagement.createTarget(entityFactory.generateTarget("fsdfsd")); + targetManagement.createTarget(entityFactory.generateTarget("fsdfsd")); final DistributionSet set = testdataFactory.createDistributionSet("one"); mvc.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/fsdfsd/assignedDS") @@ -1299,19 +1304,20 @@ public class MgmtTargetResourceTest extends AbstractRestIntegrationTest { + "\"}]"; } - private void createSingleTarget(final String controllerId, final String name) { + private Target createSingleTarget(final String controllerId, final String name) { final Target target = entityFactory.generateTarget(controllerId); target.setName(name); target.setDescription(TARGET_DESCRIPTION_TEST); targetManagement.createTarget(target); - controllerManagament.updateLastTargetQuery(controllerId, null); + return controllerManagament.updateLastTargetQuery(controllerId, null); } /** - * creating targets with the given amount by setting name, id etc from the - * alphabet [a-z] using the ASCII. - * + * Creating targets with the given amount by setting name, id etc from the + * alphabet [a-z] using ASCII. + * * @param amount + * The number of targets to create */ private void createTargetsAlphabetical(final int amount) { char character = 'a'; @@ -1320,15 +1326,15 @@ public class MgmtTargetResourceTest extends AbstractRestIntegrationTest { final Target target = entityFactory.generateTarget(str); target.setName(str); target.setDescription(str); - final Target savedTarget = targetManagement.createTarget(target); - assertThat(savedTarget.getLastModifiedBy()).isNotNull(); + targetManagement.createTarget(target); + controllerManagament.updateLastTargetQuery(str, null); character++; } } /** - * helper method to give feedback mark an target IN_SNCY - * + * helper method to give feedback mark an target IN_SYNC + * */ private void feedbackToByInSync(final Long actionId) { final Action action = deploymentManagement.findAction(actionId); @@ -1339,7 +1345,7 @@ public class MgmtTargetResourceTest extends AbstractRestIntegrationTest { /** * helper method to create a target and start an action on it. - * + * * @return The targetid of the created target. */ private Target createTargetAndStartAction() { @@ -1348,7 +1354,7 @@ public class MgmtTargetResourceTest extends AbstractRestIntegrationTest { final Target tA = targetManagement .createTarget(testdataFactory.generateTarget("target-id-A", "first description")); // assign a distribution set so we get an active update action - deploymentManagement.assignDistributionSet(dsA, Lists.newArrayList(tA)); + deploymentManagement.assignDistributionSet(dsA, newArrayList(tA)); // verify active action final Slice actionsByTarget = deploymentManagement.findActionsByTarget(new PageRequest(0, 100), tA); assertThat(actionsByTarget.getContent()).hasSize(1); diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java index 531da8819..e5dcb8f74 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java @@ -14,8 +14,8 @@ import java.util.Map; import javax.validation.constraints.NotNull; -import org.eclipse.hawkbit.eventbus.event.DownloadProgressEvent; import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; +import org.eclipse.hawkbit.repository.eventbus.event.DownloadProgressEvent; import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; import org.eclipse.hawkbit.repository.exception.ToManyAttributeEntriesException; @@ -58,16 +58,20 @@ public interface ControllerManagement { Action addCancelActionStatus(@NotNull ActionStatus actionStatus); /** - * Sends the download progress in percentage and notifies the - * {@link EventBus} with a {@link DownloadProgressEvent}. + * Sends the download progress and notifies the {@link EventBus} with a + * {@link DownloadProgressEvent}. * * @param statusId * the ID of the {@link ActionStatus} - * @param progressPercent - * the progress in percentage which must be between 0-100 + * @param requestedBytes + * requested bytes of the request + * @param shippedBytesSinceLast + * since the last report + * @param shippedBytesOverall + * for the {@link ActionStatus} */ @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) - void downloadProgressPercent(long statusId, int progressPercent); + void downloadProgress(Long statusId, Long requestedBytes, Long shippedBytesSinceLast, Long shippedBytesOverall); /** * Simple addition of a new {@link ActionStatus} entry to the {@link Action} @@ -75,9 +79,11 @@ public interface ControllerManagement { * * @param statusMessage * to add to the action + * + * @return create {@link ActionStatus} entity */ @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) - void addInformationalActionStatus(@NotNull ActionStatus statusMessage); + ActionStatus addInformationalActionStatus(@NotNull ActionStatus statusMessage); /** * Adds an {@link ActionStatus} entry for an update {@link Action} including diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java index 9dc5f74c8..270de3e74 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/DeploymentManagement.java @@ -269,8 +269,7 @@ public interface DeploymentManagement { * @return the actions referring a specific rollout and a specific parent * rollout group in a specific status */ - @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET + SpringEvalExpressions.HAS_AUTH_OR - + SpringEvalExpressions.IS_SYSTEM_CODE) + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) List findActionsByRolloutGroupParentAndStatus(@NotNull Rollout rollout, @NotNull RolloutGroup rolloutGroupParent, @NotNull Action.Status actionStatus); @@ -496,8 +495,7 @@ public interface DeploymentManagement { * the action to start now. * @return the action which has been started */ - @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET + SpringEvalExpressions.HAS_AUTH_OR - + SpringEvalExpressions.IS_SYSTEM_CODE) + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_TARGET) Action startScheduledAction(@NotNull Action action); /** diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java index 2c6e85dda..337475318 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/RolloutManagement.java @@ -61,8 +61,7 @@ public interface RolloutManagement { * this check. This check is only applied if the last check is * less than (lastcheck-delay). */ - @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_WRITE + SpringEvalExpressions.HAS_AUTH_OR - + SpringEvalExpressions.IS_SYSTEM_CODE) + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_WRITE) void checkRunningRollouts(long delayBetweenChecks); /** @@ -266,8 +265,7 @@ public interface RolloutManagement { * if given rollout is not in {@link RolloutStatus#RUNNING}. * Only running rollouts can be paused. */ - @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_WRITE + SpringEvalExpressions.HAS_AUTH_OR - + SpringEvalExpressions.IS_SYSTEM_CODE) + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_WRITE) void pauseRollout(@NotNull Rollout rollout); /** @@ -281,8 +279,7 @@ public interface RolloutManagement { * if given rollout is not in {@link RolloutStatus#PAUSED}. Only * paused rollouts can be resumed. */ - @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_WRITE + SpringEvalExpressions.HAS_AUTH_OR - + SpringEvalExpressions.IS_SYSTEM_CODE) + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_WRITE) void resumeRollout(@NotNull Rollout rollout); /** @@ -303,8 +300,7 @@ public interface RolloutManagement { * if given rollout is not in {@link RolloutStatus#READY}. Only * ready rollouts can be started. */ - @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_WRITE + SpringEvalExpressions.HAS_AUTH_OR - + SpringEvalExpressions.IS_SYSTEM_CODE) + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_WRITE) Rollout startRollout(@NotNull Rollout rollout); /** @@ -326,8 +322,7 @@ public interface RolloutManagement { * if given rollout is not in {@link RolloutStatus#READY}. Only * ready rollouts can be started. */ - @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_WRITE + SpringEvalExpressions.HAS_AUTH_OR - + SpringEvalExpressions.IS_SYSTEM_CODE) + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_ROLLOUT_MANAGEMENT_WRITE) Rollout startRolloutAsync(@NotNull Rollout rollout); /** diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SystemManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SystemManagement.java index 039e4ae13..a44b13d92 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SystemManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/SystemManagement.java @@ -39,16 +39,14 @@ public interface SystemManagement { * @param tenant * to delete */ - @PreAuthorize(SpringEvalExpressions.HAS_AUTH_SYSTEM_ADMIN + SpringEvalExpressions.HAS_AUTH_OR - + SpringEvalExpressions.IS_SYSTEM_CODE) + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_SYSTEM_ADMIN) void deleteTenant(@NotNull String tenant); /** * * @return list of all tenant names in the system. */ - @PreAuthorize(SpringEvalExpressions.HAS_AUTH_SYSTEM_ADMIN + SpringEvalExpressions.HAS_AUTH_OR - + SpringEvalExpressions.IS_SYSTEM_CODE) + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_SYSTEM_ADMIN) List findTenants(); /** @@ -68,8 +66,8 @@ public interface SystemManagement { /** * Returns {@link TenantMetaData} of given and current tenant. Creates for * new tenants also two {@link SoftwareModuleType} (os and app) and - * {@link RepositoryConstants#DEFAULT_DS_TYPES_IN_TENANT} {@link DistributionSetType}s - * (os and os_app). + * {@link RepositoryConstants#DEFAULT_DS_TYPES_IN_TENANT} + * {@link DistributionSetType}s (os and os_app). * * DISCLAIMER: this variant is used during initial login (where the tenant * is not yet in the session). Please user {@link #getTenantMetadata()} for diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TenantConfigurationManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TenantConfigurationManagement.java index 734704f05..9c467f9dd 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TenantConfigurationManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TenantConfigurationManagement.java @@ -54,8 +54,7 @@ public interface TenantConfigurationManagement { * @return if no default value is set and no database value available * or returns the tenant configuration value */ - @PreAuthorize(value = SpringEvalExpressions.HAS_AUTH_TENANT_CONFIGURATION + SpringEvalExpressions.HAS_AUTH_OR - + SpringEvalExpressions.IS_SYSTEM_CODE) + @PreAuthorize(value = SpringEvalExpressions.HAS_AUTH_TENANT_CONFIGURATION) TenantConfigurationValue buildTenantConfigurationValueByKey(TenantConfigurationKey configurationKey, Class propertyType, TenantConfiguration tenantConfiguration); @@ -87,8 +86,7 @@ public interface TenantConfigurationManagement { * if the property cannot be converted to the given * {@code propertyType} */ - @PreAuthorize(value = SpringEvalExpressions.HAS_AUTH_TENANT_CONFIGURATION + SpringEvalExpressions.HAS_AUTH_OR - + SpringEvalExpressions.IS_SYSTEM_CODE) + @PreAuthorize(value = SpringEvalExpressions.HAS_AUTH_TENANT_CONFIGURATION) TenantConfigurationValue getConfigurationValue(TenantConfigurationKey configurationKey); /** @@ -114,8 +112,7 @@ public interface TenantConfigurationManagement { * if the property cannot be converted to the given * {@code propertyType} */ - @PreAuthorize(value = SpringEvalExpressions.HAS_AUTH_TENANT_CONFIGURATION + SpringEvalExpressions.HAS_AUTH_OR - + SpringEvalExpressions.IS_SYSTEM_CODE) + @PreAuthorize(value = SpringEvalExpressions.HAS_AUTH_TENANT_CONFIGURATION) TenantConfigurationValue getConfigurationValue(TenantConfigurationKey configurationKey, Class propertyType); @@ -139,7 +136,6 @@ public interface TenantConfigurationManagement { * if the property cannot be converted to the given * {@code propertyType} */ - @PreAuthorize(value = SpringEvalExpressions.HAS_AUTH_TENANT_CONFIGURATION + SpringEvalExpressions.HAS_AUTH_OR - + SpringEvalExpressions.IS_SYSTEM_CODE) + @PreAuthorize(value = SpringEvalExpressions.HAS_AUTH_TENANT_CONFIGURATION) T getGlobalConfigurationValue(TenantConfigurationKey configurationKey, Class propertyType); } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TenantStatsManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TenantStatsManagement.java index 6d390c03c..f041bc45f 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TenantStatsManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/TenantStatsManagement.java @@ -20,15 +20,14 @@ import org.springframework.security.access.prepost.PreAuthorize; public interface TenantStatsManagement { /** - * Service for stats of a single tenant. Opens a new transaction and as a - * result can an be used for multiple tenants, i.e. to allow in one session - * to collect data of all tenants in the system. + * Service for stats of the current tenant. * - * @param tenant - * to collect for * @return collected statistics */ - @PreAuthorize(SpringEvalExpressions.HAS_AUTH_SYSTEM_ADMIN) - TenantUsage getStatsOfTenant(String tenant); + @PreAuthorize(SpringEvalExpressions.HAS_AUTH_READ_REPOSITORY + SpringEvalExpressions.HAS_AUTH_OR + + SpringEvalExpressions.HAS_AUTH_READ_TARGET + SpringEvalExpressions.HAS_AUTH_OR + + SpringEvalExpressions.HAS_AUTH_TENANT_CONFIGURATION + SpringEvalExpressions.HAS_AUTH_OR + + SpringEvalExpressions.IS_SYSTEM_CODE) + TenantUsage getStatsOfTenant(); } \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/eventbus/event/DownloadProgressEvent.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/eventbus/event/DownloadProgressEvent.java new file mode 100644 index 000000000..6d145e0ae --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/eventbus/event/DownloadProgressEvent.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.eventbus.event; + +import org.eclipse.hawkbit.eventbus.event.AbstractDistributedEvent; + +/** + * Event that contains an updated download progress for a given ActionStatus + * that was written for a download request. + * + */ +public class DownloadProgressEvent extends AbstractDistributedEvent { + + private static final long serialVersionUID = 1L; + + private final Long statusId; + private final long requestedBytes; + private final long shippedBytesSinceLast; + private final long shippedBytesOverall; + + /** + * Constructor. + * + * @param tenant + * the tenant for this event + * @param statusId + * of ActionStatus that was written for the download request + * @param requestedBytes + * bytes requested + * @param shippedBytesSinceLast + * bytes since last event + * @param shippedBytesOverall + * on the download request + */ + public DownloadProgressEvent(final String tenant, final Long statusId, final Long requestedBytes, + final Long shippedBytesSinceLast, final Long shippedBytesOverall) { + // the revision of the DownloadProgressEvent is just equal the + // shippedBytesOverall as this is a growing number. + super(shippedBytesOverall, tenant); + this.statusId = statusId; + this.requestedBytes = requestedBytes; + this.shippedBytesSinceLast = shippedBytesSinceLast; + this.shippedBytesOverall = shippedBytesOverall; + } + + public Long getStatusId() { + return statusId; + } + + public long getRequestedBytes() { + return requestedBytes; + } + + public long getShippedBytesSinceLast() { + return shippedBytesSinceLast; + } + + public long getShippedBytesOverall() { + return shippedBytesOverall; + } +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Action.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Action.java index c2b96905e..98d101471 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Action.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Action.java @@ -39,12 +39,6 @@ public interface Action extends TenantAwareBaseEntity { return Status.CANCELING.equals(getStatus()) || Status.CANCELED.equals(getStatus()); } - /** - * @return current {@link Status#DOWNLOAD} progress if known by the update - * server. - */ - int getDownloadProgressPercent(); - /** * @return current {@link Status} of the {@link Action}. */ diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/ActionStatus.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/ActionStatus.java index f3ca66887..e83108fe8 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/ActionStatus.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/ActionStatus.java @@ -43,6 +43,12 @@ public interface ActionStatus extends TenantAwareBaseEntity { */ void addMessage(String message); + /** + * @return current {@link Status#DOWNLOAD} progress if known by the update + * server. + */ + int getDownloadProgressPercent(); + /** * @return list of message entries that can be added to the * {@link ActionStatus}. diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/report/model/TenantUsage.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/report/model/TenantUsage.java index 5467099c4..933ca564b 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/report/model/TenantUsage.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/report/model/TenantUsage.java @@ -106,7 +106,7 @@ public class TenantUsage { } @Override - public int hashCode() { // NOSONAR - as this is generated code + public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (int) (actions ^ (actions >>> 32)); @@ -118,15 +118,14 @@ public class TenantUsage { } @Override - public boolean equals(final Object obj) { // NOSONAR - as this is generated - // code + public boolean equals(final Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } - if (getClass() != obj.getClass()) { + if (!(obj instanceof TenantUsage)) { return false; } final TenantUsage other = (TenantUsage) obj; @@ -154,7 +153,7 @@ public class TenantUsage { @Override public String toString() { - return "SystemUsage [tenantName=" + tenantName + ", targets=" + targets + ", artifacts=" + artifacts + return "TenantUsage [tenantName=" + tenantName + ", targets=" + targets + ", artifacts=" + artifacts + ", actions=" + actions + ", overallArtifactVolumeInBytes=" + overallArtifactVolumeInBytes + "]"; } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/RepositoryApplicationConfiguration.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/RepositoryApplicationConfiguration.java index 628af38e0..19f7b9e0f 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/RepositoryApplicationConfiguration.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/RepositoryApplicationConfiguration.java @@ -54,6 +54,7 @@ import org.eclipse.hawkbit.repository.jpa.model.helper.TenantConfigurationManage import org.eclipse.hawkbit.security.SecurityTokenGenerator; import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.tenancy.TenantAware; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -71,6 +72,8 @@ import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; +import com.google.common.eventbus.EventBus; + /** * General configuration for hawkBit's Repository. * @@ -85,6 +88,9 @@ import org.springframework.validation.beanvalidation.MethodValidationPostProcess @EnableScheduling @EntityScan("org.eclipse.hawkbit.repository.jpa.model") public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { + @Autowired + private EventBus eventBus; + /** * @return the {@link SystemSecurityContext} singleton bean which make it * accessible in beans which cannot access the service directly, @@ -249,7 +255,9 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { @Bean @ConditionalOnMissingBean public TenantStatsManagement tenantStatsManagement() { - return new JpaTenantStatsManagement(); + final TenantStatsManagement mgmt = new JpaTenantStatsManagement(); + eventBus.register(mgmt); + return mgmt; } /** diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java index b9dfce105..b574d1452 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java @@ -451,8 +451,8 @@ public class JpaControllerManagement implements ControllerManagement { @Override @Modifying @Transactional(isolation = Isolation.READ_UNCOMMITTED) - public void addInformationalActionStatus(final ActionStatus statusMessage) { - actionStatusRepository.save((JpaActionStatus) statusMessage); + public ActionStatus addInformationalActionStatus(final ActionStatus statusMessage) { + return actionStatusRepository.save((JpaActionStatus) statusMessage); } @Override @@ -469,8 +469,9 @@ public class JpaControllerManagement implements ControllerManagement { } @Override - public void downloadProgressPercent(final long statusId, final int progressPercent) { - cacheWriteNotify.downloadProgressPercent(statusId, progressPercent); + public void downloadProgress(final Long statusId, final Long requestedBytes, final Long shippedBytesSinceLast, + final Long shippedBytesOverall) { + cacheWriteNotify.downloadProgress(statusId, requestedBytes, shippedBytesSinceLast, shippedBytesOverall); } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSoftwareManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSoftwareManagement.java index 50127e227..b561667a9 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSoftwareManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSoftwareManagement.java @@ -132,7 +132,7 @@ public class JpaSoftwareManagement implements SoftwareManagement { final JpaSoftwareModuleType type = softwareModuleTypeRepository.findOne(sm.getId()); boolean updated = false; - if (sm.getDescription() != null && !sm.getDescription().equals(type.getDescription())) { + if (sm.getDescription() == null || !sm.getDescription().equals(type.getDescription())) { type.setDescription(sm.getDescription()); updated = true; } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSystemManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSystemManagement.java index ea6c99708..3b7f77b47 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSystemManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSystemManagement.java @@ -19,6 +19,7 @@ import org.eclipse.hawkbit.repository.Constants; import org.eclipse.hawkbit.repository.SystemManagement; import org.eclipse.hawkbit.repository.TenantStatsManagement; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.jpa.configuration.MultiTenantJpaTransactionManager; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetType; import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModuleType; import org.eclipse.hawkbit.repository.jpa.model.JpaTenantMetaData; @@ -26,18 +27,20 @@ import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.SoftwareModuleType; import org.eclipse.hawkbit.repository.model.TenantMetaData; import org.eclipse.hawkbit.repository.report.model.SystemUsageReport; +import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.tenancy.TenantAware; import org.eclipse.persistence.config.PersistenceUnitProperties; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.interceptor.KeyGenerator; -import org.springframework.context.ApplicationContext; import org.springframework.data.jpa.repository.Modifying; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.springframework.transaction.support.TransactionTemplate; import org.springframework.validation.annotation.Validated; /** @@ -108,7 +111,10 @@ public class JpaSystemManagement implements CurrentTenantCacheKeyGenerator, Syst private SystemManagementCacheKeyGenerator currentTenantCacheKeyGenerator; @Autowired - private ApplicationContext applicationContext; + private SystemSecurityContext systemSecurityContext; + + @Autowired + private PlatformTransactionManager txManager; @Override public SystemUsageReport getSystemUsageStatistics() { @@ -147,7 +153,7 @@ public class JpaSystemManagement implements CurrentTenantCacheKeyGenerator, Syst final List tenants = findTenants(); tenants.forEach(tenant -> tenantAware.runAsTenant(tenant, () -> { - report.addTenantData(systemStatsManagement.getStatsOfTenant(tenant)); + report.addTenantData(systemStatsManagement.getStatsOfTenant()); return null; })); } @@ -159,39 +165,57 @@ public class JpaSystemManagement implements CurrentTenantCacheKeyGenerator, Syst } @Override - @Cacheable(value = "tenantMetadata", key = "#tenant.toUpperCase()") @Transactional(isolation = Isolation.READ_UNCOMMITTED) @Modifying public TenantMetaData getTenantMetadata(final String tenant) { final TenantMetaData result = tenantMetaDataRepository.findByTenantIgnoreCase(tenant); - // Create if it does not exist if (result == null) { try { currentTenantCacheKeyGenerator.getCreateInitialTenant().set(tenant); - cacheManager.getCache("currentTenant").evict(currentTenantKeyGenerator().generate(null, null)); - applicationContext.getBean("currentTenantKeyGenerator"); - return tenantMetaDataRepository.save(new JpaTenantMetaData(createStandardSoftwareDataSetup(), tenant)); + return createInitialTenantMetaData(tenant); + } finally { currentTenantCacheKeyGenerator.getCreateInitialTenant().remove(); } } - return result; } + /** + * Creating the initial tenant meta-data in a new transaction. Due the + * {@link MultiTenantJpaTransactionManager} is using the current tenant to + * set the necessary tenant discriminator to the query. This is not working + * if we don't have a current tenant set. Due the + * {@link #getTenantMetadata(String)} is maybe called without having a + * current tenant we need to re-open a new transaction so the + * {@link MultiTenantJpaTransactionManager} is called again and set the + * tenant for this transaction. + * + * @param tenant + * the tenant to be created + * @return the initial created {@link TenantMetaData} + */ + private TenantMetaData createInitialTenantMetaData(final String tenant) { + final DefaultTransactionDefinition def = new DefaultTransactionDefinition(); + def.setName("initial-tenant-creation"); + def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + return systemSecurityContext.runAsSystemAsTenant( + () -> new TransactionTemplate(txManager, def).execute(status -> tenantMetaDataRepository + .save(new JpaTenantMetaData(createStandardSoftwareDataSetup(), tenant))), + tenant); + } + @Override public List findTenants() { return tenantMetaDataRepository.findAll().stream().map(md -> md.getTenant()).collect(Collectors.toList()); } @Override - @CacheEvict(value = { "tenantMetadata" }, key = "#tenant.toUpperCase()") @Transactional(isolation = Isolation.READ_UNCOMMITTED) @Modifying public void deleteTenant(final String tenant) { cacheManager.evictCaches(tenant); - cacheManager.getCache("currentTenant").evict(currentTenantKeyGenerator().generate(null, null)); tenantAware.runAsTenant(tenant, () -> { entityManager.setProperty(PersistenceUnitProperties.MULTITENANT_PROPERTY_DEFAULT, tenant.toUpperCase()); tenantMetaDataRepository.deleteByTenantIgnoreCase(tenant); @@ -214,7 +238,6 @@ public class JpaSystemManagement implements CurrentTenantCacheKeyGenerator, Syst } @Override - @Cacheable(value = "tenantMetadata", keyGenerator = "tenantKeyGenerator") @Transactional(isolation = Isolation.READ_UNCOMMITTED) @Modifying public TenantMetaData getTenantMetadata() { @@ -226,7 +249,7 @@ public class JpaSystemManagement implements CurrentTenantCacheKeyGenerator, Syst } @Override - @Cacheable(value = "currentTenant", keyGenerator = "currentTenantKeyGenerator") + @Cacheable(value = "currentTenant", keyGenerator = "currentTenantKeyGenerator", cacheManager = "directCacheManager") // set transaction to not supported, due we call this in // BaseEntity#prePersist methods // and it seems that JPA committing the transaction when executing this @@ -249,7 +272,6 @@ public class JpaSystemManagement implements CurrentTenantCacheKeyGenerator, Syst } @Override - @CachePut(value = "tenantMetadata", key = "#metaData.tenant.toUpperCase()") @Transactional(isolation = Isolation.READ_UNCOMMITTED) @Modifying public TenantMetaData updateTenantMetadata(final TenantMetaData metaData) { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTenantStatsManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTenantStatsManagement.java index 75f99d886..83f37304a 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTenantStatsManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTenantStatsManagement.java @@ -12,8 +12,8 @@ import java.util.Optional; import org.eclipse.hawkbit.repository.TenantStatsManagement; import org.eclipse.hawkbit.repository.report.model.TenantUsage; +import org.eclipse.hawkbit.tenancy.TenantAware; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -35,9 +35,14 @@ public class JpaTenantStatsManagement implements TenantStatsManagement { @Autowired private ActionRepository actionRepository; + @Autowired + private TenantAware tenantAware; + @Override @Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_UNCOMMITTED) - public TenantUsage getStatsOfTenant(final String tenant) { + public TenantUsage getStatsOfTenant() { + final String tenant = tenantAware.getCurrentTenant(); + final TenantUsage result = new TenantUsage(tenant); result.setTargets(targetRepository.count()); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/cache/CacheWriteNotify.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/cache/CacheWriteNotify.java index fdaf868e8..6dfdf157f 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/cache/CacheWriteNotify.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/cache/CacheWriteNotify.java @@ -8,9 +8,10 @@ */ package org.eclipse.hawkbit.repository.jpa.cache; -import org.eclipse.hawkbit.eventbus.event.DownloadProgressEvent; +import java.math.RoundingMode; + +import org.eclipse.hawkbit.repository.eventbus.event.DownloadProgressEvent; import org.eclipse.hawkbit.repository.eventbus.event.RolloutGroupCreatedEvent; -import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.ActionStatus; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.tenancy.TenantAware; @@ -20,6 +21,7 @@ import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; import com.google.common.eventbus.EventBus; +import com.google.common.math.DoubleMath; /** * An service which combines the functionality for functional use cases to write @@ -30,10 +32,6 @@ import com.google.common.eventbus.EventBus; */ @Service public class CacheWriteNotify { - - /** - * - */ private static final int DOWNLOAD_PROGRESS_MAX = 100; @Autowired @@ -46,20 +44,29 @@ public class CacheWriteNotify { private TenantAware tenantAware; /** - * writes the download progress in percentage into the cache + * writes the download progress into the cache * {@link CacheKeys#DOWNLOAD_PROGRESS_PERCENT} and notifies the * {@link EventBus} with a {@link DownloadProgressEvent}. * * @param statusId * the ID of the {@link ActionStatus} - * @param progressPercent - * the progress in percentage which must be between 0-100 + * @param requestedBytes + * requested bytes of the request + * @param shippedBytesSinceLast + * since last event + * @param shippedBytesOverall + * for the download request */ - public void downloadProgressPercent(final long statusId, final int progressPercent) { + public void downloadProgress(final Long statusId, final Long requestedBytes, final Long shippedBytesSinceLast, + final Long shippedBytesOverall) { - final Cache cache = cacheManager.getCache(Action.class.getName()); + final Cache cache = cacheManager.getCache(ActionStatus.class.getName()); final String cacheKey = CacheKeys.entitySpecificCacheKey(String.valueOf(statusId), CacheKeys.DOWNLOAD_PROGRESS_PERCENT); + + final int progressPercent = DoubleMath.roundToInt(shippedBytesOverall * 100.0 / requestedBytes, + RoundingMode.DOWN); + if (progressPercent < DOWNLOAD_PROGRESS_MAX) { cache.put(cacheKey, progressPercent); } else { @@ -69,7 +76,8 @@ public class CacheWriteNotify { cache.evict(cacheKey); } - eventBus.post(new DownloadProgressEvent(tenantAware.getCurrentTenant(), statusId, progressPercent)); + eventBus.post(new DownloadProgressEvent(tenantAware.getCurrentTenant(), statusId, requestedBytes, + shippedBytesSinceLast, shippedBytesOverall)); } /** diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaAction.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaAction.java index 2b8744618..5fa2c3bed 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaAction.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaAction.java @@ -27,10 +27,7 @@ import javax.persistence.NamedEntityGraphs; import javax.persistence.NamedSubgraph; import javax.persistence.OneToMany; import javax.persistence.Table; -import javax.persistence.Transient; -import org.eclipse.hawkbit.repository.jpa.cache.CacheField; -import org.eclipse.hawkbit.repository.jpa.cache.CacheKeys; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.ActionStatus; import org.eclipse.hawkbit.repository.model.DistributionSet; @@ -89,13 +86,6 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio @JoinColumn(name = "rollout", foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_action_rollout")) private JpaRollout rollout; - /** - * Note: filled only in {@link Status#DOWNLOAD}. - */ - @Transient - @CacheField(key = CacheKeys.DOWNLOAD_PROGRESS_PERCENT) - private int downloadProgressPercent; - @Override public DistributionSet getDistributionSet() { return distributionSet; @@ -120,15 +110,6 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio this.status = status; } - @Override - public int getDownloadProgressPercent() { - return downloadProgressPercent; - } - - public void setDownloadProgressPercent(final int downloadProgressPercent) { - this.downloadProgressPercent = downloadProgressPercent; - } - @Override public boolean isActive() { return active; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaActionStatus.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaActionStatus.java index ef1ad3d1e..3a8cb8683 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaActionStatus.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaActionStatus.java @@ -24,7 +24,10 @@ import javax.persistence.ManyToOne; import javax.persistence.NamedAttributeNode; import javax.persistence.NamedEntityGraph; import javax.persistence.Table; +import javax.persistence.Transient; +import org.eclipse.hawkbit.repository.jpa.cache.CacheField; +import org.eclipse.hawkbit.repository.jpa.cache.CacheKeys; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.ActionStatus; @@ -63,6 +66,13 @@ public class JpaActionStatus extends AbstractJpaTenantAwareBaseEntity implements @Column(name = "detail_message", length = 512) private final List messages = new ArrayList<>(); + /** + * Note: filled only in {@link Status#DOWNLOAD}. + */ + @Transient + @CacheField(key = CacheKeys.DOWNLOAD_PROGRESS_PERCENT) + private int downloadProgressPercent; + /** * Creates a new {@link ActionStatus} object. * @@ -105,6 +115,11 @@ public class JpaActionStatus extends AbstractJpaTenantAwareBaseEntity implements // JPA default constructor. } + @Override + public int getDownloadProgressPercent() { + return downloadProgressPercent; + } + @Override public Long getOccurredAt() { return occurredAt; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/DistributionSetSpecification.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/DistributionSetSpecification.java index 4ee849bcc..a2c0802a0 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/DistributionSetSpecification.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/DistributionSetSpecification.java @@ -83,6 +83,7 @@ public final class DistributionSetSpecification { targetRoot.fetch(JpaDistributionSet_.modules, JoinType.LEFT); targetRoot.fetch(JpaDistributionSet_.tags, JoinType.LEFT); targetRoot.fetch(JpaDistributionSet_.type, JoinType.LEFT); + targetRoot.fetch(JpaDistributionSet_.metadata, JoinType.LEFT); query.distinct(true); return predicate; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/SoftwareModuleSpecification.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/SoftwareModuleSpecification.java index feba50ac8..044383d49 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/SoftwareModuleSpecification.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/specifications/SoftwareModuleSpecification.java @@ -8,6 +8,7 @@ */ package org.eclipse.hawkbit.repository.jpa.specifications; +import javax.persistence.criteria.JoinType; import javax.persistence.criteria.Predicate; import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModule; @@ -38,6 +39,8 @@ public final class SoftwareModuleSpecification { return (targetRoot, query, cb) -> { final Predicate predicate = cb.equal(targetRoot. get(JpaSoftwareModule_.id), moduleId); targetRoot.fetch(JpaSoftwareModule_.type); + targetRoot.fetch(JpaSoftwareModule_.metadata,JoinType.LEFT); + query.distinct(true); return predicate; }; } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/SystemManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/SystemManagementTest.java index a89486757..a9f004a73 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/SystemManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/SystemManagementTest.java @@ -17,6 +17,7 @@ import java.util.Random; import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModule; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TenantMetaData; import org.eclipse.hawkbit.repository.report.model.TenantUsage; import org.eclipse.hawkbit.repository.test.util.WithSpringAuthorityRule; import org.junit.Test; @@ -29,6 +30,15 @@ import ru.yandex.qatools.allure.annotations.Stories; @Stories("System Management") public class SystemManagementTest extends AbstractJpaIntegrationTestWithMongoDB { + @Test + @Description("Ensures that you can create a tenant without setting the necessary security context which holds a current tenant") + public void createInitialTenantWithoutSecurityContext() { + securityRule.clear(); + final String tenantToBeCreated = "newTenantToCreate"; + final TenantMetaData tenantMetadata = systemManagement.getTenantMetadata(tenantToBeCreated); + assertThat(tenantMetadata).isNotNull(); + } + @Test @Description("Ensures that findTenants returns all tenants and not only restricted to the tenant which currently is logged in") public void findTenantsReturnsAllTenantsNotOnlyWhichLoggedIn() throws Exception { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/cache/CacheWriteNotifyTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/cache/CacheWriteNotifyTest.java index 603af17c9..1f6fbca6f 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/cache/CacheWriteNotifyTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/cache/CacheWriteNotifyTest.java @@ -13,8 +13,8 @@ import static org.mockito.Matchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import org.eclipse.hawkbit.eventbus.event.DownloadProgressEvent; -import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.eventbus.event.DownloadProgressEvent; +import org.eclipse.hawkbit.repository.model.ActionStatus; import org.eclipse.hawkbit.tenancy.TenantAware; import org.junit.Before; import org.junit.Test; @@ -59,15 +59,14 @@ public class CacheWriteNotifyTest { @Test public void downloadgProgressIsCachedAndEventSent() { final long knownStatusId = 1; - final int knownPercentage = 23; - when(cacheManagerMock.getCache(Action.class.getName())).thenReturn(cacheMock); + when(cacheManagerMock.getCache(ActionStatus.class.getName())).thenReturn(cacheMock); when(tenantAwareMock.getCurrentTenant()).thenReturn("default"); - underTest.downloadProgressPercent(knownStatusId, knownPercentage); + underTest.downloadProgress(knownStatusId, 500L, 100L, 100L); - verify(cacheManagerMock).getCache(eq(Action.class.getName())); - verify(cacheMock).put(knownStatusId + "." + CacheKeys.DOWNLOAD_PROGRESS_PERCENT, knownPercentage); + verify(cacheManagerMock).getCache(eq(ActionStatus.class.getName())); + verify(cacheMock).put(knownStatusId + "." + CacheKeys.DOWNLOAD_PROGRESS_PERCENT, 20); verify(eventBusMock).post(any(DownloadProgressEvent.class)); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLTargetFieldTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLTargetFieldTest.java index 2cccd655b..cdac51614 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLTargetFieldTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLTargetFieldTest.java @@ -37,6 +37,8 @@ import ru.yandex.qatools.allure.annotations.Stories; @Features("Component Tests - Repository") @Stories("RSQL filter target") public class RSQLTargetFieldTest extends AbstractJpaIntegrationTest { + private static final long LAST_TARGET_QUERY = 10000; + private static final long LAST_TARGET_QUERY_SMALLER = 1000; @Before public void seuptBeforeTest() { @@ -48,14 +50,16 @@ public class RSQLTargetFieldTest extends AbstractJpaIntegrationTest { final TargetInfo targetInfo = target.getTargetInfo(); targetInfo.getControllerAttributes().put("revision", "1.1"); ((JpaTargetInfo) target.getTargetInfo()).setUpdateStatus(TargetUpdateStatus.PENDING); - + ((JpaTargetInfo) target.getTargetInfo()).setLastTargetQuery(LAST_TARGET_QUERY); targetManagement.createTarget(target); + final JpaTarget target2 = new JpaTarget("targetId1234"); target2.setDescription("targetId1234"); - final TargetInfo targetInfo2 = new JpaTargetInfo(target2); + final TargetInfo targetInfo2 = target2.getTargetInfo(); targetInfo2.getControllerAttributes().put("revision", "1.2"); - target2.setTargetInfo(targetInfo2); + ((JpaTargetInfo) target2.getTargetInfo()).setLastTargetQuery(LAST_TARGET_QUERY_SMALLER); targetManagement.createTarget(target2); + targetManagement.createTarget(new JpaTarget("targetId1235")); targetManagement.createTarget(new JpaTarget("targetId1236")); @@ -166,6 +170,17 @@ public class RSQLTargetFieldTest extends AbstractJpaIntegrationTest { assertRSQLQuery(TargetFields.TAG.name() + "=out=(Tag1,notexist)", 0); } + @Test + @Description("Test filter target by lastTargetQuery") + public void testFilterByLastTargetQuery() { + assertRSQLQuery(TargetFields.LASTCONTROLLERREQUESTAT.name() + "==" + LAST_TARGET_QUERY, 1); + assertRSQLQuery(TargetFields.LASTCONTROLLERREQUESTAT.name() + "!=" + LAST_TARGET_QUERY, 1); + assertRSQLQuery(TargetFields.LASTCONTROLLERREQUESTAT.name() + "=lt=" + LAST_TARGET_QUERY, 1); + assertRSQLQuery(TargetFields.LASTCONTROLLERREQUESTAT.name() + "=lt=" + LAST_TARGET_QUERY_SMALLER, 0); + assertRSQLQuery(TargetFields.LASTCONTROLLERREQUESTAT.name() + "=gt=" + LAST_TARGET_QUERY_SMALLER, 1); + assertRSQLQuery(TargetFields.LASTCONTROLLERREQUESTAT.name() + "=gt=" + LAST_TARGET_QUERY, 0); + } + private void assertRSQLQuery(final String rsqlParam, final long expcetedTargets) { final Page findTargetPage = targetManagement.findTargetsAll(rsqlParam, new PageRequest(0, 100)); final long countTargetsAll = findTargetPage.getTotalElements(); diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/WithSpringAuthorityRule.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/WithSpringAuthorityRule.java index 8f5f8e94b..a2bc90555 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/WithSpringAuthorityRule.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/WithSpringAuthorityRule.java @@ -63,7 +63,7 @@ public class WithSpringAuthorityRule implements TestRule { } return oldContext; } - + /** * @param annotation */ @@ -129,6 +129,14 @@ public class WithSpringAuthorityRule implements TestRule { private void after(final SecurityContext oldContext) { SecurityContextHolder.setContext(oldContext); } + + /** + * Clears the current security context. + */ + public void clear() + { + SecurityContextHolder.clearContext(); + } /** * @param callable diff --git a/hawkbit-rest-core/src/main/java/org/eclipse/hawkbit/rest/util/RestResourceConversionHelper.java b/hawkbit-rest-core/src/main/java/org/eclipse/hawkbit/rest/util/RestResourceConversionHelper.java index 875152e0e..0c9dd39c5 100644 --- a/hawkbit-rest-core/src/main/java/org/eclipse/hawkbit/rest/util/RestResourceConversionHelper.java +++ b/hawkbit-rest-core/src/main/java/org/eclipse/hawkbit/rest/util/RestResourceConversionHelper.java @@ -23,6 +23,7 @@ import javax.servlet.http.HttpServletResponse; import org.eclipse.hawkbit.artifact.repository.model.DbArtifact; import org.eclipse.hawkbit.repository.ControllerManagement; +import org.eclipse.hawkbit.repository.model.ActionStatus; import org.eclipse.hawkbit.repository.model.LocalArtifact; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -87,7 +88,7 @@ public final class RestResourceConversionHelper { * @param controllerManagement * to write progress updates to * @param statusId - * of the UpdateActionStatus + * of the {@link ActionStatus} * * @return http code * @@ -293,6 +294,7 @@ public final class RestResourceConversionHelper { long toRead = length; boolean toContinue = true; + long shippedSinceLastEvent = 0; while (toContinue) { final int r = from.read(buf); @@ -304,9 +306,11 @@ public final class RestResourceConversionHelper { if (toRead > 0) { to.write(buf, 0, r); total += r; + shippedSinceLastEvent += r; } else { to.write(buf, 0, (int) toRead + r); total += toRead + r; + shippedSinceLastEvent += toRead + r; toContinue = false; } @@ -316,7 +320,8 @@ public final class RestResourceConversionHelper { // every 10 percent an event if (newPercent == 100 || newPercent > progressPercent + 10) { progressPercent = newPercent; - controllerManagement.downloadProgressPercent(statusId, progressPercent); + controllerManagement.downloadProgress(statusId, length, shippedSinceLastEvent, total); + shippedSinceLastEvent = 0; } } } diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java index 6a68aee5b..daf5f0dd9 100644 --- a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java @@ -224,8 +224,10 @@ public final class SpPermission { /* * Spring security eval expressions. */ - private static final String HAS_AUTH_PREFIX = "hasAuthority('"; - private static final String HAS_AUTH_SUFFIX = "')"; + private static final String BRACKET_OPEN = "("; + private static final String BRACKET_CLOSE = ")"; + private static final String HAS_AUTH_PREFIX = "hasAuthority" + BRACKET_OPEN + "'"; + private static final String HAS_AUTH_SUFFIX = "'" + BRACKET_CLOSE; private static final String HAS_AUTH_AND = " and "; /** @@ -257,99 +259,6 @@ public final class SpPermission { */ public static final String HAS_AUTH_OR = " or "; - /** - * Spring security eval hasAuthority expression to check if spring - * context contains {@link SpPermission#UPDATE_TARGET}. - */ - public static final String HAS_AUTH_UPDATE_TARGET = HAS_AUTH_PREFIX + UPDATE_TARGET + HAS_AUTH_SUFFIX; - - /** - * Spring security eval hasAuthority expression to check if spring - * context contains {@link SpPermission#SYSTEM_ADMIN}. - */ - public static final String HAS_AUTH_SYSTEM_ADMIN = HAS_AUTH_PREFIX + SYSTEM_ADMIN + HAS_AUTH_SUFFIX; - - /** - * Spring security eval hasAuthority expression to check if spring - * context contains {@link SpPermission#READ_TARGET}. - */ - public static final String HAS_AUTH_READ_TARGET = HAS_AUTH_PREFIX + READ_TARGET + HAS_AUTH_SUFFIX; - - /** - * Spring security eval hasAuthority expression to check if spring - * context contains {@link SpPermission#CREATE_TARGET}. - */ - public static final String HAS_AUTH_CREATE_TARGET = HAS_AUTH_PREFIX + CREATE_TARGET + HAS_AUTH_SUFFIX; - - /** - * Spring security eval hasAuthority expression to check if spring - * context contains {@link SpPermission#DELETE_TARGET}. - */ - public static final String HAS_AUTH_DELETE_TARGET = HAS_AUTH_PREFIX + DELETE_TARGET + HAS_AUTH_SUFFIX; - - /** - * Spring security eval hasAuthority expression to check if spring - * context contains {@link SpPermission#READ_REPOSITORY} and - * {@link SpPermission#UPDATE_TARGET}. - */ - public static final String HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET = HAS_AUTH_PREFIX + READ_REPOSITORY - + HAS_AUTH_SUFFIX + HAS_AUTH_AND + HAS_AUTH_PREFIX + UPDATE_TARGET + HAS_AUTH_SUFFIX; - - /** - * Spring security eval hasAuthority expression to check if spring - * context contains {@link SpPermission#CREATE_REPOSITORY}. - */ - public static final String HAS_AUTH_CREATE_REPOSITORY = HAS_AUTH_PREFIX + CREATE_REPOSITORY + HAS_AUTH_SUFFIX; - - /** - * Spring security eval hasAuthority expression to check if spring - * context contains {@link SpPermission#DELETE_REPOSITORY}. - */ - public static final String HAS_AUTH_DELETE_REPOSITORY = HAS_AUTH_PREFIX + DELETE_REPOSITORY + HAS_AUTH_SUFFIX; - - /** - * Spring security eval hasAuthority expression to check if spring - * context contains {@link SpPermission#READ_REPOSITORY}. - */ - public static final String HAS_AUTH_READ_REPOSITORY = HAS_AUTH_PREFIX + READ_REPOSITORY + HAS_AUTH_SUFFIX; - - /** - * Spring security eval hasAuthority expression to check if spring - * context contains {@link SpPermission#UPDATE_REPOSITORY}. - */ - public static final String HAS_AUTH_UPDATE_REPOSITORY = HAS_AUTH_PREFIX + UPDATE_REPOSITORY + HAS_AUTH_SUFFIX; - - /** - * Spring security eval hasAuthority expression to check if spring - * context contains {@link SpPermission#READ_REPOSITORY} and - * {@link SpPermission#READ_TARGET}. - */ - public static final String HAS_AUTH_READ_REPOSITORY_AND_READ_TARGET = HAS_AUTH_PREFIX + READ_REPOSITORY - + HAS_AUTH_SUFFIX + HAS_AUTH_AND + HAS_AUTH_PREFIX + READ_TARGET + HAS_AUTH_SUFFIX; - - /** - * Spring security eval hasAuthority expression to check if spring - * context contains {@link SpPermission#DOWNLOAD_REPOSITORY_ARTIFACT}. - */ - public static final String HAS_AUTH_DOWNLOAD_ARTIFACT = HAS_AUTH_PREFIX + DOWNLOAD_REPOSITORY_ARTIFACT - + HAS_AUTH_SUFFIX; - - /** - * Spring security eval hasAnyRole expression to check if the spring - * context contains the anoynmous role or the controller specific role - * {@link SpPermission#CONTROLLER_ROLE}. - */ - public static final String IS_CONTROLLER = "hasAnyRole('" + CONTROLLER_ROLE_ANONYMOUS + "', '" + CONTROLLER_ROLE - + "')"; - - /** - * Spring security eval hasAuthority expression to check if the spring - * context contains the role to allow controllers to download specific - * role {@link SpPermission#CONTROLLER_DOWNLOAD_ROLE}. - */ - public static final String HAS_CONTROLLER_DOWNLOAD = HAS_AUTH_PREFIX + CONTROLLER_DOWNLOAD_ROLE - + HAS_AUTH_SUFFIX; - /** * Spring security eval hasAnyRole expression to check if the spring * context contains system code role @@ -359,47 +268,168 @@ public final class SpPermission { /** * Spring security eval hasAuthority expression to check if spring - * context contains {@link SpPermission#CREATE_REPOSITORY} and - * {@link SpPermission#CREATE_TARGET}. + * context contains {@link SpPermission#UPDATE_TARGET} or + * {@link #IS_SYSTEM_CODE}. */ - public static final String HAS_AUTH_CREATE_REPOSITORY_AND_CREATE_TARGET = HAS_AUTH_PREFIX + CREATE_REPOSITORY - + HAS_AUTH_SUFFIX + HAS_AUTH_AND + HAS_AUTH_PREFIX + CREATE_TARGET + HAS_AUTH_SUFFIX; + public static final String HAS_AUTH_UPDATE_TARGET = HAS_AUTH_PREFIX + UPDATE_TARGET + HAS_AUTH_SUFFIX + + HAS_AUTH_OR + IS_SYSTEM_CODE; /** * Spring security eval hasAuthority expression to check if spring - * context contains {@link SpPermission#ROLLOUT_MANAGEMENT} + * context contains {@link SpPermission#SYSTEM_ADMIN} or + * {@link #IS_SYSTEM_CODE}. + */ + public static final String HAS_AUTH_SYSTEM_ADMIN = HAS_AUTH_PREFIX + SYSTEM_ADMIN + HAS_AUTH_SUFFIX + + HAS_AUTH_OR + IS_SYSTEM_CODE; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#READ_TARGET} or + * {@link #IS_SYSTEM_CODE}. + */ + public static final String HAS_AUTH_READ_TARGET = HAS_AUTH_PREFIX + READ_TARGET + HAS_AUTH_SUFFIX + HAS_AUTH_OR + + IS_SYSTEM_CODE; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#CREATE_TARGET} or + * {@link #IS_SYSTEM_CODE}. + */ + public static final String HAS_AUTH_CREATE_TARGET = HAS_AUTH_PREFIX + CREATE_TARGET + HAS_AUTH_SUFFIX + + HAS_AUTH_OR + IS_SYSTEM_CODE; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#DELETE_TARGET} or + * {@link #IS_SYSTEM_CODE}. + */ + public static final String HAS_AUTH_DELETE_TARGET = HAS_AUTH_PREFIX + DELETE_TARGET + HAS_AUTH_SUFFIX + + HAS_AUTH_OR + IS_SYSTEM_CODE; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#READ_REPOSITORY} and + * {@link SpPermission#UPDATE_TARGET} or {@link #IS_SYSTEM_CODE}. + */ + public static final String HAS_AUTH_READ_REPOSITORY_AND_UPDATE_TARGET = BRACKET_OPEN + HAS_AUTH_PREFIX + + READ_REPOSITORY + HAS_AUTH_SUFFIX + HAS_AUTH_AND + HAS_AUTH_PREFIX + UPDATE_TARGET + HAS_AUTH_SUFFIX + + BRACKET_CLOSE + HAS_AUTH_OR + IS_SYSTEM_CODE; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#CREATE_REPOSITORY} or + * {@link #IS_SYSTEM_CODE}. + */ + public static final String HAS_AUTH_CREATE_REPOSITORY = HAS_AUTH_PREFIX + CREATE_REPOSITORY + HAS_AUTH_SUFFIX + + HAS_AUTH_OR + IS_SYSTEM_CODE; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#DELETE_REPOSITORY} or + * {@link #IS_SYSTEM_CODE}. + */ + public static final String HAS_AUTH_DELETE_REPOSITORY = HAS_AUTH_PREFIX + DELETE_REPOSITORY + HAS_AUTH_SUFFIX + + HAS_AUTH_OR + IS_SYSTEM_CODE; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#READ_REPOSITORY} or + * {@link #IS_SYSTEM_CODE}. + */ + public static final String HAS_AUTH_READ_REPOSITORY = HAS_AUTH_PREFIX + READ_REPOSITORY + HAS_AUTH_SUFFIX + + HAS_AUTH_OR + IS_SYSTEM_CODE; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#UPDATE_REPOSITORY} or + * {@link #IS_SYSTEM_CODE}. + */ + public static final String HAS_AUTH_UPDATE_REPOSITORY = HAS_AUTH_PREFIX + UPDATE_REPOSITORY + HAS_AUTH_SUFFIX + + HAS_AUTH_OR + IS_SYSTEM_CODE; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#READ_REPOSITORY} and + * {@link SpPermission#READ_TARGET} or {@link #IS_SYSTEM_CODE}. + */ + public static final String HAS_AUTH_READ_REPOSITORY_AND_READ_TARGET = BRACKET_OPEN + HAS_AUTH_PREFIX + + READ_REPOSITORY + HAS_AUTH_SUFFIX + HAS_AUTH_AND + HAS_AUTH_PREFIX + READ_TARGET + HAS_AUTH_SUFFIX + + BRACKET_CLOSE + HAS_AUTH_OR + IS_SYSTEM_CODE; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#DOWNLOAD_REPOSITORY_ARTIFACT} or + * {@link #IS_SYSTEM_CODE}. + */ + public static final String HAS_AUTH_DOWNLOAD_ARTIFACT = HAS_AUTH_PREFIX + DOWNLOAD_REPOSITORY_ARTIFACT + + HAS_AUTH_SUFFIX + HAS_AUTH_OR + IS_SYSTEM_CODE; + + /** + * Spring security eval hasAnyRole expression to check if the spring + * context contains the anoynmous role or the controller specific role + * {@link SpringEvalExpressions#CONTROLLER_ROLE}. + */ + public static final String IS_CONTROLLER = "hasAnyRole('" + CONTROLLER_ROLE_ANONYMOUS + "', '" + CONTROLLER_ROLE + + "')"; + + /** + * Spring security eval hasAuthority expression to check if the spring + * context contains the role to allow controllers to download specific + * role {@link SpringEvalExpressions#CONTROLLER_DOWNLOAD_ROLE} + */ + public static final String HAS_CONTROLLER_DOWNLOAD = HAS_AUTH_PREFIX + CONTROLLER_DOWNLOAD_ROLE + + HAS_AUTH_SUFFIX; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#CREATE_REPOSITORY} and + * {@link SpPermission#CREATE_TARGET} or {@link #IS_SYSTEM_CODE}. + */ + public static final String HAS_AUTH_CREATE_REPOSITORY_AND_CREATE_TARGET = BRACKET_OPEN + HAS_AUTH_PREFIX + + CREATE_REPOSITORY + HAS_AUTH_SUFFIX + HAS_AUTH_AND + HAS_AUTH_PREFIX + CREATE_TARGET + HAS_AUTH_SUFFIX + + BRACKET_CLOSE + HAS_AUTH_OR + IS_SYSTEM_CODE; + + /** + * Spring security eval hasAuthority expression to check if spring + * context contains {@link SpPermission#ROLLOUT_MANAGEMENT} or + * {@link #IS_SYSTEM_CODE}. */ public static final String HAS_AUTH_ROLLOUT_MANAGEMENT_READ = HAS_AUTH_PREFIX + ROLLOUT_MANAGEMENT - + HAS_AUTH_SUFFIX; + + HAS_AUTH_SUFFIX + HAS_AUTH_OR + IS_SYSTEM_CODE; /** * Spring security eval hasAuthority expression to check if spring * context contains {@link SpPermission#ROLLOUT_MANAGEMENT} and - * {@link SpPermission#READ_TARGET} + * {@link SpPermission#READ_TARGET} or {@link #IS_SYSTEM_CODE}. */ - public static final String HAS_AUTH_ROLLOUT_MANAGEMENT_READ_AND_TARGET_READ = HAS_AUTH_PREFIX - + ROLLOUT_MANAGEMENT + HAS_AUTH_SUFFIX + HAS_AUTH_AND + HAS_AUTH_PREFIX + READ_TARGET + HAS_AUTH_SUFFIX; + public static final String HAS_AUTH_ROLLOUT_MANAGEMENT_READ_AND_TARGET_READ = BRACKET_OPEN + HAS_AUTH_PREFIX + + ROLLOUT_MANAGEMENT + HAS_AUTH_SUFFIX + HAS_AUTH_AND + HAS_AUTH_PREFIX + READ_TARGET + HAS_AUTH_SUFFIX + + BRACKET_CLOSE + HAS_AUTH_OR + IS_SYSTEM_CODE; /** * Spring security eval hasAuthority expression to check if spring * context contains {@link SpPermission#ROLLOUT_MANAGEMENT} and - * {@link SpPermission#UPDATE_TARGET}. + * {@link SpPermission#UPDATE_TARGET} or {@link #IS_SYSTEM_CODE}. */ - public static final String HAS_AUTH_ROLLOUT_MANAGEMENT_WRITE = HAS_AUTH_PREFIX + ROLLOUT_MANAGEMENT - + HAS_AUTH_SUFFIX + HAS_AUTH_AND + HAS_AUTH_PREFIX + UPDATE_TARGET + HAS_AUTH_SUFFIX; + public static final String HAS_AUTH_ROLLOUT_MANAGEMENT_WRITE = BRACKET_OPEN + HAS_AUTH_PREFIX + + ROLLOUT_MANAGEMENT + HAS_AUTH_SUFFIX + HAS_AUTH_AND + HAS_AUTH_PREFIX + UPDATE_TARGET + + HAS_AUTH_SUFFIX + BRACKET_CLOSE + HAS_AUTH_OR + IS_SYSTEM_CODE; /** * Spring security eval hasAuthority expression to check if spring - * context contains {@link SpPermission#TENANT_CONFIGURATION} + * context contains {@link SpPermission#TENANT_CONFIGURATION} or + * {@link #IS_SYSTEM_CODE}. */ public static final String HAS_AUTH_TENANT_CONFIGURATION = HAS_AUTH_PREFIX + TENANT_CONFIGURATION - + HAS_AUTH_SUFFIX; + + HAS_AUTH_SUFFIX + HAS_AUTH_OR + IS_SYSTEM_CODE; /** * Spring security eval hasAuthority expression to check if spring - * context contains {@link SpPermission#SYSTEM_MONITOR} + * context contains {@link SpPermission#SYSTEM_MONITOR} or + * {@link #IS_SYSTEM_CODE}. */ - public static final String HAS_AUTH_SYSTEM_MONITOR = HAS_AUTH_PREFIX + SYSTEM_MONITOR + HAS_AUTH_SUFFIX; + public static final String HAS_AUTH_SYSTEM_MONITOR = HAS_AUTH_PREFIX + SYSTEM_MONITOR + HAS_AUTH_SUFFIX + + HAS_AUTH_OR + IS_SYSTEM_CODE; private SpringEvalExpressions() { // utility class diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SecurityContextTenantAware.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SecurityContextTenantAware.java index 4b5fe7224..f2cfbbdbf 100644 --- a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SecurityContextTenantAware.java +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SecurityContextTenantAware.java @@ -9,6 +9,7 @@ package org.eclipse.hawkbit.security; import java.util.Collection; +import java.util.Collections; import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails; import org.eclipse.hawkbit.tenancy.TenantAware; @@ -80,32 +81,37 @@ public class SecurityContextTenantAware implements TenantAware { @Override public boolean equals(final Object another) { - return delegate.equals(another); + if (delegate != null) { + return delegate.equals(another); + } else if (another == null) { + return true; + } + return false; } @Override public String toString() { - return delegate.toString(); + return (delegate != null) ? delegate.toString() : null; } @Override public int hashCode() { - return delegate.hashCode(); + return (delegate != null) ? delegate.hashCode() : -1; } @Override public String getName() { - return delegate.getName(); + return (delegate != null) ? delegate.getName() : null; } @Override public Collection getAuthorities() { - return delegate.getAuthorities(); + return (delegate != null) ? delegate.getAuthorities() : Collections.emptyList(); } @Override public Object getCredentials() { - return delegate.getCredentials(); + return (delegate != null) ? delegate.getCredentials() : null; } @Override @@ -115,16 +121,19 @@ public class SecurityContextTenantAware implements TenantAware { @Override public Object getPrincipal() { - return delegate.getPrincipal(); + return (delegate != null) ? delegate.getPrincipal() : null; } @Override public boolean isAuthenticated() { - return delegate.isAuthenticated(); + return (delegate != null) ? delegate.isAuthenticated() : true; } @Override - public void setAuthenticated(final boolean isAuthenticated) throws IllegalArgumentException { + public void setAuthenticated(final boolean isAuthenticated) { + if (delegate == null) { + return; + } delegate.setAuthenticated(isAuthenticated); } } diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SystemSecurityContext.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SystemSecurityContext.java index c0ecb8ceb..da998c87c 100644 --- a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SystemSecurityContext.java +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SystemSecurityContext.java @@ -60,6 +60,9 @@ public class SystemSecurityContext { * The security context will be switched to the system code and back after * the callable is called. * + * The system code is executed for a current tenant by using the + * {@link TenantAware#getCurrentTenant()}. + * * @param callable * the callable to call within the system security context * @return the return value of the {@link Callable#call()} method. @@ -67,12 +70,36 @@ public class SystemSecurityContext { // Exception squid:S2221 - Callable declares Exception @SuppressWarnings("squid:S2221") public T runAsSystem(final Callable callable) { + return runAsSystemAsTenant(callable, tenantAware.getCurrentTenant()); + } + + /** + * Runs a given {@link Callable} within a system security context, which is + * permitted to call secured system code. Often the system needs to call + * secured methods by it's own without relying on the current security + * context e.g. if the current security context does not contain the + * necessary permission it's necessary to execute code as system code to + * execute necessary methods and functionality. + * + * The security context will be switched to the system code and back after + * the callable is called. + * + * The system code is executed for a specific given tenant by using the + * {@link TenantAware}. + * + * @param callable + * the callable to call within the system security context + * @param tenant + * the tenant to act as system code + * @return the return value of the {@link Callable#call()} method. + */ + public T runAsSystemAsTenant(final Callable callable, final String tenant) { final SecurityContext oldContext = SecurityContextHolder.getContext(); try { logger.debug("entering system code execution"); - return tenantAware.runAsTenant(tenantAware.getCurrentTenant(), () -> { + return tenantAware.runAsTenant(tenant, () -> { try { - setSystemContext(oldContext); + setSystemContext(SecurityContextHolder.getContext()); return callable.call(); } catch (final Exception e) { throw Throwables.propagate(e); @@ -100,6 +127,13 @@ public class SystemSecurityContext { SecurityContextHolder.setContext(securityContextImpl); } + /** + * An implementation of the Spring's {@link Authentication} object which is + * used within a system security code block and wraps the original + * authentication object. The wrapped object contains the necessary + * {@link SpringEvalExpressions#SYSTEM_ROLE} which is allowed to execute all + * secured methods. + */ public static class SystemCodeAuthentication implements Authentication { private static final long serialVersionUID = 1L; diff --git a/hawkbit-ui/pom.xml b/hawkbit-ui/pom.xml index 08d225c62..b90e36e88 100644 --- a/hawkbit-ui/pom.xml +++ b/hawkbit-ui/pom.xml @@ -200,6 +200,10 @@ org.springframework.security spring-security-web + + org.apache.commons + commons-collections4 + com.vaadin diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleAddUpdateWindow.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleAddUpdateWindow.java index 6a5a07a40..c8eb74313 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleAddUpdateWindow.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleAddUpdateWindow.java @@ -10,6 +10,8 @@ package org.eclipse.hawkbit.ui.artifacts.smtable; import java.io.Serializable; +import javax.annotation.PostConstruct; + import org.eclipse.hawkbit.repository.EntityFactory; import org.eclipse.hawkbit.repository.SoftwareManagement; import org.eclipse.hawkbit.repository.model.SoftwareModule; @@ -18,18 +20,17 @@ import org.eclipse.hawkbit.ui.common.CommonDialogWindow; import org.eclipse.hawkbit.ui.common.SoftwareModuleTypeBeanQuery; import org.eclipse.hawkbit.ui.common.table.BaseEntityEventType; import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; +import org.eclipse.hawkbit.ui.decorators.SPUIWindowDecorator; import org.eclipse.hawkbit.ui.utils.HawkbitCommonUtil; import org.eclipse.hawkbit.ui.utils.I18N; import org.eclipse.hawkbit.ui.utils.SPUIComponentIdProvider; import org.eclipse.hawkbit.ui.utils.SPUIDefinitions; import org.eclipse.hawkbit.ui.utils.SPUILabelDefinitions; -import org.eclipse.hawkbit.ui.utils.SPUIStyleDefinitions; import org.eclipse.hawkbit.ui.utils.UINotification; import org.springframework.beans.factory.annotation.Autowired; import org.vaadin.addons.lazyquerycontainer.BeanQueryFactory; import org.vaadin.spring.events.EventBus; -import com.vaadin.event.FieldEvents.TextChangeEvent; import com.vaadin.spring.annotation.SpringComponent; import com.vaadin.spring.annotation.ViewScope; import com.vaadin.ui.ComboBox; @@ -38,8 +39,6 @@ import com.vaadin.ui.FormLayout; import com.vaadin.ui.Label; import com.vaadin.ui.TextArea; import com.vaadin.ui.TextField; -import com.vaadin.ui.UI; -import com.vaadin.ui.Window; import com.vaadin.ui.themes.ValoTheme; /** @@ -66,8 +65,6 @@ public class SoftwareModuleAddUpdateWindow extends CustomComponent implements Se @Autowired private transient EntityFactory entityFactory; - private Label mandatoryLabel; - private TextField nameTextField; private TextField versionTextField; @@ -80,14 +77,20 @@ public class SoftwareModuleAddUpdateWindow extends CustomComponent implements Se private CommonDialogWindow window; - private String oldDescriptionValue; - - private String oldVendorValue; - private Boolean editSwModule = Boolean.FALSE; private Long baseSwModuleId; + private FormLayout formLayout; + + /** + * Initialize Distribution Add and Edit Window. + */ + @PostConstruct + void init() { + createRequiredComponents(); + } + /** * Create window for new software module. * @@ -95,11 +98,7 @@ public class SoftwareModuleAddUpdateWindow extends CustomComponent implements Se * module. */ public CommonDialogWindow createAddSoftwareModuleWindow() { - - editSwModule = Boolean.FALSE; - createRequiredComponents(); - createWindow(); - return window; + return createUpdateSoftwareModuleWindow(null); } /** @@ -110,17 +109,11 @@ public class SoftwareModuleAddUpdateWindow extends CustomComponent implements Se * @return reference of {@link com.vaadin.ui.Window} to update software * module. */ - public Window createUpdateSoftwareModuleWindow(final Long baseSwModuleId) { - - editSwModule = Boolean.TRUE; + public CommonDialogWindow createUpdateSoftwareModuleWindow(final Long baseSwModuleId) { this.baseSwModuleId = baseSwModuleId; - createRequiredComponents(); - createWindow(); - /* populate selected target values to edit. */ + resetComponents(); populateValuesOfSwModule(); - nameTextField.setEnabled(false); - versionTextField.setEnabled(false); - typeComboBox.setEnabled(false); + createWindow(); return window; } @@ -145,13 +138,6 @@ public class SoftwareModuleAddUpdateWindow extends CustomComponent implements Se ValoTheme.TEXTAREA_TINY, false, null, i18n.get("textfield.description"), SPUILabelDefinitions.TEXT_AREA_MAX_LENGTH); descTextArea.setId(SPUIComponentIdProvider.ADD_SW_MODULE_DESCRIPTION); - addDescriptionTextChangeListener(); - addVendorTextChangeListener(); - - /* Label for mandatory symbol */ - mandatoryLabel = new Label(i18n.get("label.mandatory.field")); - mandatoryLabel.setStyleName(SPUIStyleDefinitions.SP_TEXTFIELD_ERROR); - mandatoryLabel.addStyleName(ValoTheme.LABEL_SMALL); typeComboBox = SPUIComponentProvider.getComboBox(i18n.get("upload.swmodule.type"), "", "", null, null, true, null, i18n.get("upload.swmodule.type")); @@ -159,46 +145,34 @@ public class SoftwareModuleAddUpdateWindow extends CustomComponent implements Se typeComboBox.setStyleName(SPUIDefinitions.COMBO_BOX_SPECIFIC_STYLE + " " + ValoTheme.COMBOBOX_TINY); typeComboBox.setNewItemsAllowed(Boolean.FALSE); typeComboBox.setImmediate(Boolean.TRUE); - populateTypeNameCombo(); - - resetOldValues(); } - /** - * - */ private void populateTypeNameCombo() { typeComboBox.setContainerDataSource(HawkbitCommonUtil.createLazyQueryContainer( new BeanQueryFactory(SoftwareModuleTypeBeanQuery.class))); typeComboBox.setItemCaptionPropertyId(SPUILabelDefinitions.VAR_NAME); - } - private void resetOldValues() { - oldDescriptionValue = null; - oldVendorValue = null; + private void resetComponents() { + + vendorTextField.clear(); + nameTextField.clear(); + versionTextField.clear(); + descTextArea.clear(); + typeComboBox.clear(); + editSwModule = Boolean.FALSE; } - /** - * Build the window content and get an instance of customDialogWindow - * - */ private void createWindow() { - final Label madatoryStarLabel = new Label("*"); madatoryStarLabel.setStyleName("v-caption v-required-field-indicator"); madatoryStarLabel.setWidth(null); - - /* - * The main layout of the window contains mandatory info, textboxes - * (controller Id, name & description) and action buttons layout - */ addStyleName("lay-color"); setSizeUndefined(); - final FormLayout formLayout = new FormLayout(); - formLayout.addComponent(mandatoryLabel); + formLayout = new FormLayout(); + formLayout.setCaption(null); formLayout.addComponent(typeComboBox); formLayout.addComponent(nameTextField); formLayout.addComponent(versionTextField); @@ -207,24 +181,17 @@ public class SoftwareModuleAddUpdateWindow extends CustomComponent implements Se setCompositionRoot(formLayout); - /* add main layout to the window */ - window = SPUIComponentProvider.getWindow(i18n.get("upload.caption.add.new.swmodule"), null, - SPUIDefinitions.CREATE_UPDATE_WINDOW, this, event -> saveOrUpdate(), event -> closeThisWindow(), null); + window = SPUIWindowDecorator.getWindow(i18n.get("upload.caption.add.new.swmodule"), null, + SPUIDefinitions.CREATE_UPDATE_WINDOW, this, event -> saveOrUpdate(), null, null, formLayout, i18n); window.getButtonsLayout().removeStyleName("actionButtonsMargin"); - nameTextField.focus(); + + nameTextField.setEnabled(!editSwModule); + versionTextField.setEnabled(!editSwModule); + typeComboBox.setEnabled(!editSwModule); + + typeComboBox.focus(); } - private void addDescriptionTextChangeListener() { - descTextArea.addTextChangeListener(event -> window.setSaveButtonEnabled(hasDescriptionChanged(event))); - } - - private void addVendorTextChangeListener() { - vendorTextField.addTextChangeListener(event -> window.setSaveButtonEnabled(hasVendorChanged(event))); - } - - /** - * Add new SW module. - */ private void addNewBaseSoftware() { final String name = HawkbitCommonUtil.trimAndNullIfEmpty(nameTextField.getValue()); final String version = HawkbitCommonUtil.trimAndNullIfEmpty(versionTextField.getValue()); @@ -232,10 +199,6 @@ public class SoftwareModuleAddUpdateWindow extends CustomComponent implements Se final String description = HawkbitCommonUtil.trimAndNullIfEmpty(descTextArea.getValue()); final String type = typeComboBox.getValue() != null ? typeComboBox.getValue().toString() : null; - if (!mandatoryCheck(name, version, type)) { - return; - } - if (HawkbitCommonUtil.isDuplicate(name, version, type)) { uiNotifcation.displayValidationError( i18n.get("message.duplicate.softwaremodule", new Object[] { name, version })); @@ -248,8 +211,6 @@ public class SoftwareModuleAddUpdateWindow extends CustomComponent implements Se new Object[] { newBaseSoftwareModule.getName() + ":" + newBaseSoftwareModule.getVersion() })); eventBus.publish(this, new SoftwareModuleEvent(BaseEntityEventType.NEW_ENTITY, newBaseSoftwareModule)); } - // close the window - closeThisWindow(); } } @@ -269,13 +230,16 @@ public class SoftwareModuleAddUpdateWindow extends CustomComponent implements Se eventBus.publish(this, new SoftwareModuleEvent(BaseEntityEventType.UPDATED_ENTITY, newSWModule)); } - closeThisWindow(); } /** * fill the data of a softwareModule in the content of the window */ private void populateValuesOfSwModule() { + if (baseSwModuleId == null) { + return; + } + editSwModule = Boolean.TRUE; final SoftwareModule swModle = softwareManagement.findSoftwareModuleById(baseSwModuleId); nameTextField.setValue(swModle.getName()); versionTextField.setValue(swModle.getVersion()); @@ -283,49 +247,10 @@ public class SoftwareModuleAddUpdateWindow extends CustomComponent implements Se : HawkbitCommonUtil.trimAndNullIfEmpty(swModle.getVendor())); descTextArea.setValue(swModle.getDescription() == null ? HawkbitCommonUtil.SP_STRING_EMPTY : HawkbitCommonUtil.trimAndNullIfEmpty(swModle.getDescription())); - oldDescriptionValue = descTextArea.getValue(); - oldVendorValue = vendorTextField.getValue(); if (swModle.getType().isDeleted()) { typeComboBox.addItem(swModle.getType().getName()); } typeComboBox.setValue(swModle.getType().getName()); - window.setSaveButtonEnabled(Boolean.FALSE); - } - - /** - * Method to close window. - */ - private void closeThisWindow() { - window.close(); - UI.getCurrent().removeWindow(window); - } - - /** - * Validation check - Mandatory. - * - * @param name - * as String - * @param version - * as version - * @return boolena as flag - */ - private boolean mandatoryCheck(final String name, final String version, final String type) { - boolean isValid = true; - if (name == null || version == null || type == null) { - if (name == null) { - nameTextField.addStyleName(SPUIStyleDefinitions.SP_TEXTFIELD_ERROR); - } - if (version == null) { - versionTextField.addStyleName(SPUIStyleDefinitions.SP_TEXTFIELD_ERROR); - } - if (type == null) { - typeComboBox.addStyleName(SPUIStyleDefinitions.SP_COMBOFIELD_ERROR); - } - - uiNotifcation.displayValidationError(i18n.get("message.mandatory.check")); - isValid = false; - } - return isValid; } private void saveOrUpdate() { @@ -336,12 +261,8 @@ public class SoftwareModuleAddUpdateWindow extends CustomComponent implements Se } } - private boolean hasDescriptionChanged(final TextChangeEvent event) { - return !(event.getText().equals(oldDescriptionValue) && vendorTextField.getValue().equals(oldVendorValue)); - } - - private boolean hasVendorChanged(final TextChangeEvent event) { - return !(event.getText().equals(oldVendorValue) && descTextArea.getValue().equals(oldDescriptionValue)); + public FormLayout getFormLayout() { + return formLayout; } } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleDetails.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleDetails.java index 653994a17..15904169d 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleDetails.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleDetails.java @@ -8,11 +8,17 @@ */ package org.eclipse.hawkbit.ui.artifacts.smtable; +import org.eclipse.hawkbit.repository.EntityFactory; +import org.eclipse.hawkbit.repository.SoftwareManagement; import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata; import org.eclipse.hawkbit.ui.artifacts.event.SoftwareModuleEvent; import org.eclipse.hawkbit.ui.artifacts.state.ArtifactUploadState; import org.eclipse.hawkbit.ui.common.detailslayout.AbstractNamedVersionedEntityTableDetailsLayout; +import org.eclipse.hawkbit.ui.common.detailslayout.SoftwareModuleMetadatadetailslayout; import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; +import org.eclipse.hawkbit.ui.distributions.event.MetadataEvent; +import org.eclipse.hawkbit.ui.distributions.smtable.SwMetadataPopupLayout; import org.eclipse.hawkbit.ui.utils.HawkbitCommonUtil; import org.eclipse.hawkbit.ui.utils.SPUIComponentIdProvider; import org.springframework.beans.factory.annotation.Autowired; @@ -45,6 +51,44 @@ public class SoftwareModuleDetails extends AbstractNamedVersionedEntityTableDeta @Autowired private ArtifactUploadState artifactUploadState; + @Autowired + private transient SoftwareManagement softwareManagement; + + @Autowired + private SwMetadataPopupLayout swMetadataPopupLayout; + + @Autowired + private EntityFactory entityFactory; + + private SoftwareModuleMetadatadetailslayout swmMetadataTable; + + /** + * softwareLayout Initialize the component. + */ + @Override + protected void init() { + swmMetadataTable = new SoftwareModuleMetadatadetailslayout(); + swmMetadataTable.init(getI18n(), getPermissionChecker(),softwareManagement,swMetadataPopupLayout,entityFactory); + super.init(); + } + + @EventBusListenerMethod(scope = EventScope.SESSION) + void onEvent(final MetadataEvent event) { + UI.getCurrent() + .access(() -> { + SoftwareModuleMetadata softwareModuleMetadata = event.getSoftwareModuleMetadata(); + if (softwareModuleMetadata != null + && isSoftwareModuleSelected(softwareModuleMetadata.getSoftwareModule())) { + if (event.getMetadataUIEvent() == MetadataEvent.MetadataUIEvent.CREATE_SOFTWARE_MODULE_METADATA) { + swmMetadataTable.createMetadata(event.getSoftwareModuleMetadata().getKey()); + } else if (event.getMetadataUIEvent() == MetadataEvent.MetadataUIEvent.DELETE_SOFTWARE_MODULE_METADATA) { + swmMetadataTable.deleteMetadata(event.getSoftwareModuleMetadata().getKey()); + } + } + }); + } + + @Override protected String getEditButtonId() { return SPUIComponentIdProvider.UPLOAD_SW_MODULE_EDIT_BUTTON; @@ -55,8 +99,9 @@ public class SoftwareModuleDetails extends AbstractNamedVersionedEntityTableDeta detailsTab.addTab(createDetailsLayout(), getI18n().get("caption.tab.details"), null); detailsTab.addTab(createDescriptionLayout(), getI18n().get("caption.tab.description"), null); detailsTab.addTab(createLogLayout(), getI18n().get("caption.logs.tab"), null); + detailsTab.addTab(swmMetadataTable, getI18n().get("caption.metadata"), null); } - + @Override protected void onEdit(final ClickEvent event) { final Window addSoftwareModule = softwareModuleAddUpdateWindow @@ -81,6 +126,8 @@ public class SoftwareModuleDetails extends AbstractNamedVersionedEntityTableDeta updateSoftwareModuleDetailsLayout(HawkbitCommonUtil.SP_STRING_EMPTY, HawkbitCommonUtil.SP_STRING_EMPTY, maxAssign); } + + populateMetadataDetails(); } private void updateSoftwareModuleDetailsLayout(final String type, final String vendor, final String maxAssign) { @@ -141,4 +188,38 @@ public class SoftwareModuleDetails extends AbstractNamedVersionedEntityTableDeta protected String getDetailsHeaderCaptionId() { return SPUIComponentIdProvider.TARGET_DETAILS_HEADER_LABEL_ID; } + + + @Override + protected void populateMetadataDetails(){ + swmMetadataTable.populateSMMetadata(getSelectedBaseEntity()); + } + + private boolean isSoftwareModuleSelected(SoftwareModule softwareModule) { + final SoftwareModule selectedUploadSWModule = artifactUploadState.getSelectedBaseSoftwareModule().isPresent() ? artifactUploadState + .getSelectedBaseSoftwareModule().get() : null; + return softwareModule != null && selectedUploadSWModule != null + && selectedUploadSWModule.getName().equals(softwareModule.getName()) + && selectedUploadSWModule.getVersion().equals(softwareModule.getVersion()); + } + + @Override + protected Boolean isMetadataIconToBeDisplayed() { + return true; + } + + @Override + protected String getShowMetadataButtonId() { + SoftwareModule selectedBaseEntity = getSelectedBaseEntity(); + return SPUIComponentIdProvider.SW_TABLE_MANAGE_METADATA_ID + "." + selectedBaseEntity.getName() + "." + + selectedBaseEntity.getVersion(); + } + + @Override + protected void showMetadata(ClickEvent event) { + SoftwareModule swmodule = softwareManagement.findSoftwareModuleWithDetails(getSelectedBaseEntityId()); + /* display the window */ + UI.getCurrent().addWindow(swMetadataPopupLayout.getWindow(swmodule,null)); + } + } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleTable.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleTable.java index 99fe185e5..1cfd28718 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleTable.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/artifacts/smtable/SoftwareModuleTable.java @@ -21,10 +21,14 @@ import org.eclipse.hawkbit.ui.artifacts.event.UploadViewAcceptCriteria; import org.eclipse.hawkbit.ui.artifacts.state.ArtifactUploadState; import org.eclipse.hawkbit.ui.common.table.AbstractNamedVersionTable; import org.eclipse.hawkbit.ui.common.table.BaseEntityEventType; +import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; +import org.eclipse.hawkbit.ui.decorators.SPUIButtonStyleSmallNoBorder; +import org.eclipse.hawkbit.ui.distributions.smtable.SwMetadataPopupLayout; import org.eclipse.hawkbit.ui.utils.HawkbitCommonUtil; import org.eclipse.hawkbit.ui.utils.SPUIComponentIdProvider; import org.eclipse.hawkbit.ui.utils.SPUIDefinitions; import org.eclipse.hawkbit.ui.utils.SPUILabelDefinitions; +import org.eclipse.hawkbit.ui.utils.SPUIStyleDefinitions; import org.eclipse.hawkbit.ui.utils.TableColumn; import org.springframework.beans.factory.annotation.Autowired; import org.vaadin.addons.lazyquerycontainer.BeanQueryFactory; @@ -38,13 +42,15 @@ import com.vaadin.data.Item; import com.vaadin.event.dd.DragAndDropEvent; import com.vaadin.event.dd.DropHandler; import com.vaadin.event.dd.acceptcriteria.AcceptCriterion; +import com.vaadin.server.FontAwesome; import com.vaadin.spring.annotation.SpringComponent; import com.vaadin.spring.annotation.ViewScope; +import com.vaadin.ui.Button; +import com.vaadin.ui.Table; import com.vaadin.ui.UI; /** * Header of Software module table. - * */ @SpringComponent @ViewScope @@ -60,6 +66,9 @@ public class SoftwareModuleTable extends AbstractNamedVersionTable showMetadataDetails((Long) itemId, nameVersionStr)); + return manageMetaDataBtn; + } + }); + } + @Override protected List getTableVisibleColumns() { final List columnList = super.getTableVisibleColumns(); if (!isMaximized()) { + columnList.add(new TableColumn(SPUILabelDefinitions.METADATA_ICON, "", 0.1F)); return columnList; } columnList.add(new TableColumn(SPUILabelDefinitions.VAR_VENDOR, i18n.get("header.vendor"), 0.1F)); @@ -212,4 +237,26 @@ public class SoftwareModuleTable extends AbstractNamedVersionTable + * E id the entity for which metadata is displayed + * @param + * M is the metadata + * + */ +public abstract class AbstractMetadataPopupLayout + extends CustomComponent { + + private static final String DELETE_BUTTON = "DELETE_BUTTON"; + + private static final long serialVersionUID = -1491218218453167613L; + + private static final String VALUE = "value"; + + private static final String KEY = "key"; + + @Autowired + protected I18N i18n; + + @Autowired + private UINotification uiNotification; + + @Autowired + protected transient EventBus.SessionEventBus eventBus; + + private TextField keyTextField; + + private TextArea valueTextArea; + + private Button addIcon; + + private Grid metaDataGrid; + + private Label headerCaption; + + private CommonDialogWindow metadataWindow; + + private E selectedEntity; + + private HorizontalLayout mainLayout; + + @PostConstruct + private void init() { + createComponents(); + buildLayout(); + + } + + /** + * Returns metadata popup. + * + * @param entity + * entity for which metadata data is displayed + * @param metaData + * metadata to be selected + * @return @link{CommonDialogWindow} + */ + public CommonDialogWindow getWindow(final E entity, final M metaData) { + selectedEntity = entity; + final String nameVersion = HawkbitCommonUtil.getFormattedNameVersion(entity.getName(), entity.getVersion()); + metadataWindow = SPUIWindowDecorator.getWindow(getMetadataCaption(nameVersion), null, + SPUIDefinitions.CUSTOM_METADATA_WINDOW, this, event -> onSave(), event -> onCancel(), null, mainLayout, + i18n); + metadataWindow.setId(SPUIComponentIdProvider.METADATA_POPUP_ID); + metadataWindow.setHeight(550, Unit.PIXELS); + metadataWindow.setWidth(800, Unit.PIXELS); + metadataWindow.getMainLayout().setSizeFull(); + metadataWindow.getButtonsLayout().setHeight("45px"); + setUpDetails(entity.getId(), metaData); + return metadataWindow; + } + + public E getSelectedEntity() { + return selectedEntity; + } + + public void setSelectedEntity(final E selectedEntity) { + this.selectedEntity = selectedEntity; + } + + protected abstract void checkForDuplicate(E entity, String value); + + protected abstract M createMetadata(E entity, String key, String value); + + protected abstract M updateMetadata(E entity, String key, String value); + + protected abstract List getMetadataList(); + + protected abstract void deleteMetadata(E entity, String key, String value); + + protected abstract boolean hasCreatePermission(); + + protected abstract boolean hasUpdatePermission(); + + private void createComponents() { + keyTextField = createKeyTextField(); + valueTextArea = createValueTextField(); + metaDataGrid = createMetadataGrid(); + addIcon = createAddIcon(); + headerCaption = createHeaderCaption(); + } + + private void buildLayout() { + final HorizontalLayout headerLayout = new HorizontalLayout(); + headerLayout.addStyleName(SPUIStyleDefinitions.WIDGET_TITLE); + headerLayout.setSpacing(false); + headerLayout.setMargin(false); + headerLayout.setSizeFull(); + headerLayout.addComponent(headerCaption); + if (hasCreatePermission()) { + headerLayout.addComponents(addIcon); + headerLayout.setComponentAlignment(addIcon, Alignment.MIDDLE_RIGHT); + } + headerLayout.setExpandRatio(headerCaption, 1.0F); + + + final HorizontalLayout headerWrapperLayout = new HorizontalLayout(); + headerWrapperLayout.addStyleName("bordered-layout" + " " + "no-border-bottom" + " " + "metadata-table-margin"); + headerWrapperLayout.addComponent(headerLayout); + headerWrapperLayout.setWidth("100%"); + headerLayout.setHeight("30px"); + + final VerticalLayout tableLayout = new VerticalLayout(); + tableLayout.setSizeFull(); + tableLayout.setHeight("100%"); + tableLayout.addComponent(headerWrapperLayout); + tableLayout.addComponent(metaDataGrid); + tableLayout.addStyleName("table-layout"); + tableLayout.setExpandRatio(metaDataGrid, 1.0F); + + final VerticalLayout metadataFieldsLayout = new VerticalLayout(); + metadataFieldsLayout.setSizeFull(); + metadataFieldsLayout.setHeight("100%"); + metadataFieldsLayout.addComponent(keyTextField); + metadataFieldsLayout.addComponent(valueTextArea); + metadataFieldsLayout.setSpacing(true); + metadataFieldsLayout.setExpandRatio(valueTextArea, 1F); + + mainLayout = new HorizontalLayout(); + mainLayout.addComponent(tableLayout); + mainLayout.addComponent(metadataFieldsLayout); + mainLayout.setExpandRatio(tableLayout, 0.5F); + mainLayout.setExpandRatio(metadataFieldsLayout, 0.5F); + mainLayout.setSizeFull(); + mainLayout.setSpacing(true); + setCompositionRoot(mainLayout); + setSizeFull(); + } + + private TextField createKeyTextField() { + final TextField keyField = SPUIComponentProvider.getTextField(i18n.get("textfield.key"), "", + ValoTheme.TEXTFIELD_TINY, true, "", i18n.get("textfield.key"), true, 128); + keyField.setId(SPUIComponentIdProvider.METADATA_KEY_FIELD_ID); + keyField.addTextChangeListener(event -> onKeyChange(event)); + keyField.setTextChangeEventMode(TextChangeEventMode.EAGER); + keyField.setWidth("100%"); + return keyField; + } + + private TextArea createValueTextField() { + valueTextArea = SPUIComponentProvider.getTextArea(i18n.get("textfield.value"), null, ValoTheme.TEXTAREA_TINY, + true, null, i18n.get("textfield.value"), 4000); + valueTextArea.setId(SPUIComponentIdProvider.METADATA_VALUE_ID); + valueTextArea.setNullRepresentation(""); + valueTextArea.setSizeFull(); + valueTextArea.setHeight(100, Unit.PERCENTAGE); + valueTextArea.addTextChangeListener(event -> onValueChange(event)); + valueTextArea.setTextChangeEventMode(TextChangeEventMode.EAGER); + return valueTextArea; + } + + private Grid createMetadataGrid() { + final Grid metadataGrid = new Grid(); + metadataGrid.addStyleName(SPUIStyleDefinitions.METADATA_GRID); + metadataGrid.setImmediate(true); + metadataGrid.setHeight("100%"); + metadataGrid.setWidth("100%"); + metadataGrid.setId(SPUIComponentIdProvider.METDATA_TABLE_ID); + metadataGrid.setSelectionMode(SelectionMode.SINGLE); + metadataGrid.setColumnReorderingAllowed(true); + metadataGrid.setContainerDataSource(getMetadataContainer()); + metadataGrid.getColumn(KEY).setHeaderCaption(i18n.get("header.key")); + metadataGrid.getColumn(VALUE).setHeaderCaption(i18n.get("header.value")); + metadataGrid.getColumn(VALUE).setHidden(true); + metadataGrid.addSelectionListener(event -> onRowClick(event)); + metadataGrid.getColumn(DELETE_BUTTON).setHeaderCaption(""); + metadataGrid.getColumn(DELETE_BUTTON).setRenderer(new HtmlButtonRenderer(event -> onDelete(event))); + metadataGrid.getColumn(DELETE_BUTTON).setWidth(50); + metadataGrid.getColumn(KEY).setExpandRatio(1); + return metadataGrid; + } + + private void onDelete(final RendererClickEvent event) { + final Item item = metaDataGrid.getContainerDataSource().getItem(event.getItemId()); + final String key = (String) item.getItemProperty(KEY).getValue(); + final String value = (String) item.getItemProperty(VALUE).getValue(); + + final ConfirmationDialog confirmDialog = new ConfirmationDialog( + i18n.get("caption.metadata.delete.action.confirmbox"), i18n.get("message.confirm.delete.metadata", key), + i18n.get("button.ok"), i18n.get("button.cancel"), ok -> { + if (ok) { + deleteMetadata(getSelectedEntity(), key, value); + uiNotification.displaySuccess(i18n.get("message.metadata.deleted.successfully", key)); + final Object selectedRow = metaDataGrid.getSelectedRow(); + metaDataGrid.getContainerDataSource().removeItem(event.getItemId()); + // force grid to refresh + metaDataGrid.clearSortOrder(); + if (!metaDataGrid.getContainerDataSource().getItemIds().isEmpty()) { + if (selectedRow != null) { + if (selectedRow.equals(event.getItemId())) { + metaDataGrid.select(metaDataGrid.getContainerDataSource().getIdByIndex(0)); + } else { + metaDataGrid.select(selectedRow); + } + } + } else { + keyTextField.clear(); + valueTextArea.clear(); + metaDataGrid.select(null); + if (hasCreatePermission()) { + keyTextField.setEnabled(true); + valueTextArea.setEnabled(true); + addIcon.setEnabled(false); + } + } + } + }); + UI.getCurrent().addWindow(confirmDialog.getWindow()); + confirmDialog.getWindow().bringToFront(); + } + + private Button createAddIcon() { + addIcon = SPUIComponentProvider.getButton(SPUIComponentIdProvider.METADTA_ADD_ICON_ID, i18n.get("button.save"), + null, null, false, FontAwesome.PLUS, SPUIButtonStyleSmallNoBorder.class); + addIcon.addClickListener(event -> onAdd(event)); + return addIcon; + } + + private Label createHeaderCaption() { + final Label captionLabel = SPUIComponentProvider.getLabel(i18n.get("caption.metadata"), + SPUILabelDefinitions.SP_WIDGET_CAPTION); + return captionLabel; + } + + private IndexedContainer getMetadataContainer() { + final IndexedContainer swcontactContainer = new IndexedContainer(); + swcontactContainer.addContainerProperty(KEY, String.class, ""); + swcontactContainer.addContainerProperty(VALUE, String.class, ""); + swcontactContainer.addContainerProperty(DELETE_BUTTON, String.class, FontAwesome.TRASH_O.getHtml()); + return swcontactContainer; + } + + private void popualateKeyValue(final Object metadataCompositeKey) { + if (metadataCompositeKey != null) { + final Item item = metaDataGrid.getContainerDataSource().getItem(metadataCompositeKey); + keyTextField.setValue((String) item.getItemProperty(KEY).getValue()); + valueTextArea.setValue((String) item.getItemProperty(VALUE).getValue()); + keyTextField.setEnabled(false); + if (hasUpdatePermission()) { + valueTextArea.setEnabled(true); + } + } + } + + private void populateGrid() { + final List metadataList = getMetadataList(); + for (final M metaData : metadataList) { + addItemToGrid(metaData.getKey(), metaData.getValue()); + } + } + + private void addItemToGrid(final String key, final String value) { + final IndexedContainer metadataContainer = (IndexedContainer) metaDataGrid.getContainerDataSource(); + final Item item = metadataContainer.addItem(key); + item.getItemProperty(VALUE).setValue(value); + item.getItemProperty(KEY).setValue(key); + } + + private void updateItemInGrid(final String key) { + final IndexedContainer metadataContainer = (IndexedContainer) metaDataGrid.getContainerDataSource(); + final Item item = metadataContainer.getItem(key); + item.getItemProperty(VALUE).setValue(valueTextArea.getValue()); + } + + private void onAdd(final ClickEvent event) { + metaDataGrid.deselect(metaDataGrid.getSelectedRow()); + valueTextArea.clear(); + keyTextField.clear(); + keyTextField.setEnabled(true); + valueTextArea.setEnabled(true); + addIcon.setEnabled(true); + } + + private void onSave() { + final String key = keyTextField.getValue(); + final String value = valueTextArea.getValue(); + if (mandatoryCheck()) { + final E entity = selectedEntity; + if (metaDataGrid.getSelectedRow() == null) { + if (!duplicateCheck(entity)) { + final M metadata = createMetadata(entity, key, value); + uiNotification.displaySuccess(i18n.get("message.metadata.saved", metadata.getKey())); + addItemToGrid(metadata.getKey(), metadata.getValue()); + metaDataGrid.scrollToEnd(); + metaDataGrid.select(metadata.getKey()); + addIcon.setEnabled(true); + metadataWindow.setSaveButtonEnabled(false); + if (!hasUpdatePermission()) { + valueTextArea.setEnabled(false); + } + } + } else { + final M metadata = updateMetadata(entity, key, value); + uiNotification.displaySuccess(i18n.get("message.metadata.updated", metadata.getKey())); + updateItemInGrid(metadata.getKey()); + metaDataGrid.select(metadata.getKey()); + addIcon.setEnabled(true); + metadataWindow.setSaveButtonEnabled(false); + } + } + } + + private boolean mandatoryCheck() { + if (keyTextField.getValue().isEmpty()) { + uiNotification.displayValidationError(i18n.get("message.key.missing")); + return false; + } + if (valueTextArea.getValue().isEmpty()) { + uiNotification.displayValidationError(i18n.get("message.value.missing")); + return false; + } + return true; + } + + private boolean duplicateCheck(final E entity) { + try { + checkForDuplicate(entity, keyTextField.getValue()); + } catch (final EntityNotFoundException exception) { + return false; + } + uiNotification.displayValidationError(i18n.get("message.metadata.duplicate.check", keyTextField.getValue())); + return true; + } + + private String getMetadataCaption(final String nameVersionStr) { + final StringBuilder caption = new StringBuilder(); + caption.append(HawkbitCommonUtil.DIV_DESCRIPTION + i18n.get("caption.metadata.popup") + " " + + HawkbitCommonUtil.getBoldHTMLText(nameVersionStr)); + caption.append(HawkbitCommonUtil.DIV_CLOSE); + return caption.toString(); + } + + private void onCancel() { + metadataWindow.close(); + UI.getCurrent().removeWindow(metadataWindow); + } + + private void onKeyChange(final TextChangeEvent event) { + if (hasCreatePermission() || hasUpdatePermission()) { + if (!valueTextArea.getValue().isEmpty() && !event.getText().isEmpty()) { + metadataWindow.setSaveButtonEnabled(true); + } else { + metadataWindow.setSaveButtonEnabled(false); + } + } + } + + private void onRowClick(final SelectionEvent event) { + final Set itemsSelected = event.getSelected(); + if (!itemsSelected.isEmpty()) { + final Object itemSelected = itemsSelected.stream().findFirst().isPresent() + ? itemsSelected.stream().findFirst().get() : null; + popualateKeyValue(itemSelected); + addIcon.setEnabled(true); + } else { + keyTextField.clear(); + valueTextArea.clear(); + if (hasCreatePermission()) { + keyTextField.setEnabled(true); + valueTextArea.setEnabled(true); + addIcon.setEnabled(false); + } else { + keyTextField.setEnabled(false); + valueTextArea.setEnabled(false); + } + } + metadataWindow.setSaveButtonEnabled(false); + } + + private void onValueChange(final TextChangeEvent event) { + if (hasCreatePermission() || hasUpdatePermission()) { + if (!keyTextField.getValue().isEmpty() && !event.getText().isEmpty()) { + metadataWindow.setSaveButtonEnabled(true); + } else { + metadataWindow.setSaveButtonEnabled(false); + } + } + } + + private void setUpDetails(final Long swId, final M metaData) { + resetDetails(); + if (swId != null) { + metaDataGrid.getContainerDataSource().removeAllItems(); + populateGrid(); + metaDataGrid.getSelectionModel().reset(); + if (!metaDataGrid.getContainerDataSource().getItemIds().isEmpty()) { + if (metaData == null) { + metaDataGrid.select(metaDataGrid.getContainerDataSource().getIdByIndex(0)); + } else { + metaDataGrid.select(metaData.getKey()); + } + } else if (hasCreatePermission()) { + keyTextField.setEnabled(true); + valueTextArea.setEnabled(true); + addIcon.setEnabled(false); + } + } + } + + private void resetDetails() { + keyTextField.clear(); + valueTextArea.clear(); + keyTextField.setEnabled(false); + valueTextArea.setEnabled(false); + metadataWindow.setSaveButtonEnabled(false); + addIcon.setEnabled(true); + } + +} \ No newline at end of file diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/CommonDialogWindow.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/CommonDialogWindow.java index 29519f162..ac59310a7 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/CommonDialogWindow.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/CommonDialogWindow.java @@ -10,32 +10,69 @@ package org.eclipse.hawkbit.ui.common; import static com.google.common.base.Preconditions.checkNotNull; -import org.apache.commons.lang3.StringUtils; -import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; -import org.eclipse.hawkbit.ui.decorators.SPUIButtonStyleBorderWithIcon; -import org.eclipse.hawkbit.ui.utils.SPUIComponentIdProvider; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.hawkbit.ui.artifacts.smtable.SoftwareModuleAddUpdateWindow; +import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; +import org.eclipse.hawkbit.ui.decorators.SPUIButtonStyleNoBorderWithIcon; +import org.eclipse.hawkbit.ui.layouts.AbstractCreateUpdateTagLayout; +import org.eclipse.hawkbit.ui.management.targettable.TargetAddUpdateWindowLayout; +import org.eclipse.hawkbit.ui.utils.I18N; +import org.eclipse.hawkbit.ui.utils.SPUIComponentIdProvider; +import org.eclipse.hawkbit.ui.utils.SPUIStyleDefinitions; +import org.vaadin.hene.flexibleoptiongroup.FlexibleOptionGroupItemComponent; + +import com.google.common.base.Strings; +import com.google.common.collect.Sets; +import com.vaadin.data.Container.ItemSetChangeEvent; +import com.vaadin.data.Container.ItemSetChangeListener; +import com.vaadin.data.Property.ValueChangeEvent; import com.vaadin.data.Property.ValueChangeListener; +import com.vaadin.data.Validator; +import com.vaadin.data.validator.NullValidator; +import com.vaadin.event.FieldEvents.TextChangeEvent; +import com.vaadin.event.FieldEvents.TextChangeListener; +import com.vaadin.event.FieldEvents.TextChangeNotifier; import com.vaadin.server.FontAwesome; +import com.vaadin.ui.AbstractComponent; +import com.vaadin.ui.AbstractField; +import com.vaadin.ui.AbstractLayout; import com.vaadin.ui.AbstractOrderedLayout; import com.vaadin.ui.Alignment; import com.vaadin.ui.Button; import com.vaadin.ui.Button.ClickListener; +import com.vaadin.ui.CheckBox; import com.vaadin.ui.Component; +import com.vaadin.ui.Field; +import com.vaadin.ui.GridLayout; import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Label; import com.vaadin.ui.Link; +import com.vaadin.ui.Table; import com.vaadin.ui.VerticalLayout; import com.vaadin.ui.Window; +import com.vaadin.ui.themes.ValoTheme; /** * - * Superclass for pop-up-windows including a minimize and close icon in the - * upper right corner and a save and cancel button at the bottom. - * + * Table pop-up-windows including a minimize and close icon in the upper right + * corner and a save and cancel button at the bottom. Is not intended to reuse. + * */ -public class CommonDialogWindow extends Window { +public class CommonDialogWindow extends Window implements Serializable { - private static final long serialVersionUID = -1321949234316858703L; + private static final long serialVersionUID = 1L; private final VerticalLayout mainLayout = new VerticalLayout(); @@ -57,6 +94,14 @@ public class CommonDialogWindow extends Window { private final ClickListener cancelButtonClickListener; + private final ClickListener close = event -> close(); + + private final transient Map orginalValues; + + private final List> allComponents; + + private final I18N i18n; + /** * Constructor. * @@ -72,38 +117,273 @@ public class CommonDialogWindow extends Window { * the cancelButtonClickListener */ public CommonDialogWindow(final String caption, final Component content, final String helpLink, - final ClickListener saveButtonClickListener, final ClickListener cancelButtonClickListener) { + final ClickListener saveButtonClickListener, final ClickListener cancelButtonClickListener, + final AbstractLayout layout, final I18N i18n) { checkNotNull(saveButtonClickListener); - checkNotNull(cancelButtonClickListener); this.caption = caption; this.content = content; this.helpLink = helpLink; this.saveButtonClickListener = saveButtonClickListener; this.cancelButtonClickListener = cancelButtonClickListener; - + this.orginalValues = new HashMap<>(); + this.allComponents = getAllComponents(layout); + this.i18n = i18n; init(); } + @Override + public void close() { + super.close(); + orginalValues.clear(); + removeListeners(); + allComponents.clear(); + this.saveButton.setEnabled(false); + } + + private void removeListeners() { + for (final AbstractField field : allComponents) { + removeTextListener(field); + removeValueChangeListener(field); + removeItemSetChangeistener(field); + } + } + + private void removeItemSetChangeistener(final AbstractField field) { + if (!(field instanceof Table)) { + return; + } + for (final Object listener : field.getListeners(ItemSetChangeEvent.class)) { + if (listener instanceof ChangeListener) { + ((Table) field).removeItemSetChangeListener((ChangeListener) listener); + } + } + } + + private void removeTextListener(final AbstractField field) { + if (!(field instanceof TextChangeNotifier)) { + return; + } + for (final Object listener : field.getListeners(TextChangeEvent.class)) { + if (listener instanceof ChangeListener) { + ((TextChangeNotifier) field).removeTextChangeListener((ChangeListener) listener); + } + } + } + + private void removeValueChangeListener(final AbstractField field) { + for (final Object listener : field.getListeners(ValueChangeEvent.class)) { + if (listener instanceof ChangeListener) { + field.removeValueChangeListener((ChangeListener) listener); + } + } + } + private final void init() { if (content instanceof AbstractOrderedLayout) { ((AbstractOrderedLayout) content).setSpacing(true); ((AbstractOrderedLayout) content).setMargin(true); } + if (content instanceof GridLayout) { + addStyleName("marginTop"); + } if (null != content) { mainLayout.addComponent(content); + mainLayout.setExpandRatio(content, 1.0F); } + + createMandatoryLabel(); + final HorizontalLayout buttonLayout = createActionButtonsLayout(); mainLayout.addComponent(buttonLayout); mainLayout.setComponentAlignment(buttonLayout, Alignment.TOP_CENTER); setCaption(caption); + setCaptionAsHtml(true); setContent(mainLayout); setResizable(false); center(); setModal(true); addStyleName("fontsize"); + setOrginaleValues(); + addListeners(); + } + + /** + * saves the original values in a Map so we can use them for detecting + * changes + */ + public final void setOrginaleValues() { + for (final AbstractField field : allComponents) { + Object value = field.getValue(); + + if (field instanceof Table) { + value = Sets.newHashSet(((Table) field).getContainerDataSource().getItemIds()); + } + orginalValues.put(field, value); + } + saveButton.setEnabled(isSaveButtonEnabledAfterValueChange(null, null)); + } + + protected void addListeners() { + addComponenetListeners(); + addCloseListenerForSaveButton(); + addCloseListenerForCancelButton(); + } + + protected void addCloseListenerForSaveButton() { + saveButton.addClickListener(close); + } + + protected void addCloseListenerForCancelButton() { + cancelButton.addClickListener(close); + } + + protected void addComponenetListeners() { + for (final AbstractField field : allComponents) { + if (field instanceof TextChangeNotifier) { + ((TextChangeNotifier) field).addTextChangeListener(new ChangeListener(field)); + } + + if (field instanceof Table) { + ((Table) field).addItemSetChangeListener(new ChangeListener(field)); + } else { + field.addValueChangeListener(new ChangeListener(field)); + } + } + } + + private boolean isSaveButtonEnabledAfterValueChange(final Component currentChangedComponent, + final Object newValue) { + return isMandatoryFieldNotEmptyAndValid(currentChangedComponent, newValue) + && isValuesChanged(currentChangedComponent, newValue); + } + + private boolean isValuesChanged(final Component currentChangedComponent, final Object newValue) { + for (final AbstractField field : allComponents) { + Object originalValue = orginalValues.get(field); + if (field instanceof CheckBox && originalValue == null) { + originalValue = Boolean.FALSE; + } + final Object currentValue = getCurrentVaue(currentChangedComponent, newValue, field); + + if (!isValueEquals(field, originalValue, currentValue)) { + return true; + } + } + return false; + } + + private boolean isValueEquals(final AbstractField field, final Object orginalValue, final Object currentValue) { + if (Set.class.equals(field.getType())) { + return CollectionUtils.isEqualCollection(CollectionUtils.emptyIfNull((Collection) orginalValue), + CollectionUtils.emptyIfNull((Collection) currentValue)); + } + + if (String.class.equals(field.getType())) { + return Objects.equals(Strings.emptyToNull((String) orginalValue), + Strings.emptyToNull((String) currentValue)); + } + + return Objects.equals(orginalValue, currentValue); + } + + private Object getCurrentVaue(final Component currentChangedComponent, final Object newValue, + final AbstractField field) { + Object currentValue = field.getValue(); + if (field instanceof Table) { + currentValue = ((Table) field).getContainerDataSource().getItemIds(); + } + + if (field.equals(currentChangedComponent)) { + currentValue = newValue; + } + return currentValue; + } + + private boolean shouldMandatoryLabelShown() { + for (final AbstractField field : allComponents) { + if (field.isRequired()) { + return true; + } + } + + return false; + } + + private boolean isMandatoryFieldNotEmptyAndValid(final Component currentChangedComponent, final Object newValue) { + + boolean valid = true; + final List> requiredComponents = allComponents.stream().filter(field -> field.isRequired()) + .collect(Collectors.toList()); + + requiredComponents.addAll(allComponents.stream().filter(this::hasNullValidator).collect(Collectors.toList())); + + for (final AbstractField field : requiredComponents) { + Object value = getCurrentVaue(currentChangedComponent, newValue, field); + + if (String.class.equals(field.getType())) { + value = Strings.emptyToNull((String) value); + } + + if (Set.class.equals(field.getType())) { + value = emptyToNull((Collection) value); + } + + if (value == null) { + return false; + } + + // We need to loop through the entire loop for validity testing. + // Otherwise the UI will only mark the + // first field with errors and then stop. If there are several + // fields with errors, this is bad. + field.setValue(value); + if (!field.isValid()) { + valid = false; + } + } + + return valid; + } + + private static Object emptyToNull(final Collection c) { + return (c == null || c.isEmpty()) ? null : c; + } + + private boolean hasNullValidator(final Component component) { + + if (component instanceof AbstractField) { + final AbstractField fieldComponent = (AbstractField) component; + for (final Validator validator : fieldComponent.getValidators()) { + if (validator instanceof NullValidator) { + return true; + } + } + } + return false; + } + + private List> getAllComponents(final AbstractLayout abstractLayout) { + final List> components = new ArrayList<>(); + + final Iterator iterate = abstractLayout.iterator(); + while (iterate.hasNext()) { + final Component c = iterate.next(); + if (c instanceof AbstractLayout) { + components.addAll(getAllComponents((AbstractLayout) c)); + } + + if (c instanceof AbstractField) { + components.add((AbstractField) c); + } + + if (c instanceof FlexibleOptionGroupItemComponent) { + components.add(((FlexibleOptionGroupItemComponent) c).getOwner()); + } + } + return components; } private HorizontalLayout createActionButtonsLayout() { @@ -111,23 +391,45 @@ public class CommonDialogWindow extends Window { buttonsLayout = new HorizontalLayout(); buttonsLayout.setSizeFull(); buttonsLayout.setSpacing(true); + buttonsLayout.setSpacing(true); + buttonsLayout.addStyleName("actionButtonsMargin"); createSaveButton(); - createCancelButton(); - buttonsLayout.addStyleName("actionButtonsMargin"); addHelpLink(); return buttonsLayout; } + private void createMandatoryLabel() { + + if (!shouldMandatoryLabelShown()) { + return; + } + + final Label mandatoryLabel = new Label(i18n.get("label.mandatory.field")); + mandatoryLabel.addStyleName(SPUIStyleDefinitions.SP_TEXTFIELD_ERROR + " " + ValoTheme.LABEL_TINY); + + if (content instanceof TargetAddUpdateWindowLayout) { + ((TargetAddUpdateWindowLayout) content).getFormLayout().addComponent(mandatoryLabel); + } else if (content instanceof SoftwareModuleAddUpdateWindow) { + ((SoftwareModuleAddUpdateWindow) content).getFormLayout().addComponent(mandatoryLabel); + } else if (content instanceof AbstractCreateUpdateTagLayout) { + ((AbstractCreateUpdateTagLayout) content).getMainLayout().addComponent(mandatoryLabel); + } + + mainLayout.addComponent(mandatoryLabel); + } + private void createCancelButton() { cancelButton = SPUIComponentProvider.getButton(SPUIComponentIdProvider.CANCEL_BUTTON, "Cancel", "", "", true, - FontAwesome.TIMES, SPUIButtonStyleBorderWithIcon.class); + FontAwesome.TIMES, SPUIButtonStyleNoBorderWithIcon.class); cancelButton.setSizeUndefined(); cancelButton.addStyleName("default-color"); - cancelButton.addClickListener(cancelButtonClickListener); + if (cancelButtonClickListener != null) { + cancelButton.addClickListener(cancelButtonClickListener); + } buttonsLayout.addComponent(cancelButton); buttonsLayout.setComponentAlignment(cancelButton, Alignment.MIDDLE_LEFT); @@ -136,10 +438,11 @@ public class CommonDialogWindow extends Window { private void createSaveButton() { saveButton = SPUIComponentProvider.getButton(SPUIComponentIdProvider.SAVE_BUTTON, "Save", "", "", true, - FontAwesome.SAVE, SPUIButtonStyleBorderWithIcon.class); + FontAwesome.SAVE, SPUIButtonStyleNoBorderWithIcon.class); saveButton.setSizeUndefined(); saveButton.addStyleName("default-color"); saveButton.addClickListener(saveButtonClickListener); + saveButton.setEnabled(false); buttonsLayout.addComponent(saveButton); buttonsLayout.setComponentAlignment(saveButton, Alignment.MIDDLE_RIGHT); buttonsLayout.setExpandRatio(saveButton, 1.0F); @@ -155,6 +458,58 @@ public class CommonDialogWindow extends Window { buttonsLayout.setComponentAlignment(helpLinkComponent, Alignment.MIDDLE_RIGHT); } + public AbstractComponent getButtonsLayout() { + return this.buttonsLayout; + } + + private class ChangeListener implements ValueChangeListener, TextChangeListener, ItemSetChangeListener { + + private final Field field; + + public ChangeListener(final Field field) { + super(); + this.field = field; + } + + @Override + public void textChange(final TextChangeEvent event) { + saveButton.setEnabled(isSaveButtonEnabledAfterValueChange(field, event.getText())); + } + + @Override + public void valueChange(final ValueChangeEvent event) { + saveButton.setEnabled(isSaveButtonEnabledAfterValueChange(field, field.getValue())); + } + + @Override + public void containerItemSetChange(final ItemSetChangeEvent event) { + if (!(field instanceof Table)) { + return; + } + final Table table = (Table) field; + saveButton.setEnabled( + isSaveButtonEnabledAfterValueChange(table, table.getContainerDataSource().getItemIds())); + } + } + + /** + * Adds the component manually to the allComponents-List and adds a + * ValueChangeListener to it. Necessary in Update Distribution Type as the + * CheckBox concerned is an ItemProperty... + * + * @param component + * AbstractField + */ + public void updateAllComponents(final AbstractField component) { + + allComponents.add(component); + component.addValueChangeListener(new ChangeListener(component)); + } + + public VerticalLayout getMainLayout() { + return mainLayout; + } + public void setSaveButtonEnabled(final boolean enabled) { saveButton.setEnabled(enabled); } @@ -162,9 +517,4 @@ public class CommonDialogWindow extends Window { public void setCancelButtonEnabled(final boolean enabled) { cancelButton.setEnabled(enabled); } - - public HorizontalLayout getButtonsLayout() { - return buttonsLayout; - } - } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/ConfirmationDialog.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/ConfirmationDialog.java index 5f20d942a..bd1f4cb9e 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/ConfirmationDialog.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/ConfirmationDialog.java @@ -89,6 +89,7 @@ public class ConfirmationDialog implements Button.ClickListener { final Button cancelButton = SPUIComponentProvider.getButton(null, cancelLabel, "", null, false, null, SPUIButtonStyleTiny.class); cancelButton.addClickListener(this); + cancelButton.setId(SPUIComponentIdProvider.CANCEL_BUTTON); window.setModal(true); window.addStyleName(SPUIStyleDefinitions.CONFIRMBOX_WINDOW_SYLE); if (this.callback == null) { diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/CustomCommonDialogWindow.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/CustomCommonDialogWindow.java new file mode 100644 index 000000000..8173735ca --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/CustomCommonDialogWindow.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.common; + +import org.eclipse.hawkbit.ui.utils.I18N; + +import com.vaadin.ui.AbstractLayout; +import com.vaadin.ui.Button.ClickListener; +import com.vaadin.ui.Component; + +public class CustomCommonDialogWindow extends CommonDialogWindow { + private static final long serialVersionUID = -4453608850403359992L; + + public CustomCommonDialogWindow(final String caption, final Component content, final String helpLink, + final ClickListener saveButtonClickListener, final ClickListener cancelButtonClickListener, + final AbstractLayout layout, final I18N i18n) { + super(caption, content, helpLink, saveButtonClickListener, cancelButtonClickListener, layout, i18n); + } + + + @Override + protected void addListeners() { + addComponenetListeners(); + addCloseListenerForCancelButton(); + } + +} \ No newline at end of file diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/detailslayout/AbstractTableDetailsLayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/detailslayout/AbstractTableDetailsLayout.java index d8029d604..b6475f647 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/detailslayout/AbstractTableDetailsLayout.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/detailslayout/AbstractTableDetailsLayout.java @@ -63,6 +63,8 @@ public abstract class AbstractTableDetailsLayout extends private Button editButton; + private Button manageMetadataBtn; + private TabSheet detailsTab; private VerticalLayout detailsLayout; @@ -136,9 +138,15 @@ public abstract class AbstractTableDetailsLayout extends SPUIButtonStyleSmallNoBorder.class); editButton.setId(getEditButtonId()); editButton.addClickListener(this::onEdit); - editButton.setEnabled(false); + manageMetadataBtn = SPUIComponentProvider.getButton("", "", "", null, false, + FontAwesome.LIST_ALT, SPUIButtonStyleSmallNoBorder.class); + manageMetadataBtn.setId(getEditButtonId()); + manageMetadataBtn.setDescription(i18n.get("tooltip.metadata.icon")); + manageMetadataBtn.addClickListener(this::showMetadata); + manageMetadataBtn.setEnabled(false); + detailsTab = SPUIComponentProvider.getDetailsTabSheet(); detailsTab.setImmediate(true); detailsTab.setWidth(98, Unit.PERCENTAGE); @@ -156,6 +164,10 @@ public abstract class AbstractTableDetailsLayout extends if (hasEditPermission()) { nameEditLayout.addComponent(editButton); nameEditLayout.setComponentAlignment(editButton, Alignment.TOP_RIGHT); + if (isMetadataIconToBeDisplayed()) { + nameEditLayout.addComponent(manageMetadataBtn); + nameEditLayout.setComponentAlignment(manageMetadataBtn, Alignment.TOP_RIGHT); + } } nameEditLayout.setExpandRatio(caption, 1.0F); nameEditLayout.addStyleName(SPUIStyleDefinitions.WIDGET_TITLE); @@ -201,6 +213,7 @@ public abstract class AbstractTableDetailsLayout extends private void populateData(final T selectedBaseEntity) { this.selectedBaseEntity = selectedBaseEntity; editButton.setEnabled(selectedBaseEntity != null); + manageMetadataBtn.setEnabled(selectedBaseEntity != null); if (selectedBaseEntity == null) { setName(getDefaultCaption(), StringUtils.EMPTY); } else { @@ -209,6 +222,7 @@ public abstract class AbstractTableDetailsLayout extends populateLog(); populateDescription(); populateDetailsWidget(); + populateMetadataDetails(); } protected void populateLog() { @@ -281,8 +295,8 @@ public abstract class AbstractTableDetailsLayout extends descriptionLayout = getTabLayout(); return descriptionLayout; } - - /** + + /** * Default caption of header to be displayed when no data row selected in * table. * @@ -327,6 +341,8 @@ public abstract class AbstractTableDetailsLayout extends } protected abstract void populateDetailsWidget(); + + protected abstract void populateMetadataDetails(); protected Long getSelectedBaseEntityId() { return selectedBaseEntity == null ? null : selectedBaseEntity.getId(); @@ -336,4 +352,10 @@ public abstract class AbstractTableDetailsLayout extends protected abstract String getName(); + protected abstract String getShowMetadataButtonId(); + + protected abstract Boolean isMetadataIconToBeDisplayed(); + + protected abstract void showMetadata(Button.ClickEvent event); + } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/detailslayout/DistributionSetMetadatadetailslayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/detailslayout/DistributionSetMetadatadetailslayout.java new file mode 100644 index 000000000..d3892a9dc --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/detailslayout/DistributionSetMetadatadetailslayout.java @@ -0,0 +1,192 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.common.detailslayout; + +import java.util.List; + +import org.eclipse.hawkbit.repository.DistributionSetManagement; +import org.eclipse.hawkbit.repository.EntityFactory; +import org.eclipse.hawkbit.repository.SpPermissionChecker; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetMetadata; +import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; +import org.eclipse.hawkbit.ui.decorators.SPUIButtonStyleSmallNoBorder; +import org.eclipse.hawkbit.ui.distributions.dstable.DsMetadataPopupLayout; +import org.eclipse.hawkbit.ui.utils.I18N; +import org.eclipse.hawkbit.ui.utils.SPUIComponentIdProvider; +import org.eclipse.hawkbit.ui.utils.SPUIStyleDefinitions; + +import com.vaadin.data.Item; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.spring.annotation.SpringComponent; +import com.vaadin.spring.annotation.VaadinSessionScope; +import com.vaadin.ui.Button; +import com.vaadin.ui.Label; +import com.vaadin.ui.Table; +import com.vaadin.ui.UI; +import com.vaadin.ui.themes.ValoTheme; + +/** + * + * DistributionSet Metadata details layout. + * + */ + +@SpringComponent +@VaadinSessionScope +public class DistributionSetMetadatadetailslayout extends Table{ + + private static final long serialVersionUID = 2913758299611837718L; + + + private DistributionSetManagement distributionSetManagement; + + private DsMetadataPopupLayout dsMetadataPopupLayout; + + private static final String METADATA_KEY = "Key"; + + private static final String VIEW ="view"; + + private SpPermissionChecker permissionChecker; + + private transient EntityFactory entityFactory; + + private I18N i18n; + + private Long selectedDistSetId; + + /** + * + * @param i18n + * @param permissionChecker + * @param distributionSetManagement + * @param dsMetadataPopupLayout + */ + public void init(final I18N i18n, final SpPermissionChecker permissionChecker, + final DistributionSetManagement distributionSetManagement, + final DsMetadataPopupLayout dsMetadataPopupLayout, + final EntityFactory entityFactory) { + this.i18n = i18n; + this.permissionChecker = permissionChecker; + this.distributionSetManagement = distributionSetManagement; + this.dsMetadataPopupLayout = dsMetadataPopupLayout; + this.entityFactory = entityFactory; + createDSMetadataTable(); + addCustomGeneratedColumns(); + } + + + /** + * Populate software module metadata. + * + * @param distributionSet + */ + public void populateDSMetadata(final DistributionSet distributionSet) { + removeAllItems(); + if (null == distributionSet) { + return; + } + selectedDistSetId = distributionSet.getId(); + final List dsMetadataList = distributionSet.getMetadata(); + if (null != dsMetadataList && !dsMetadataList.isEmpty()) { + dsMetadataList.forEach(dsMetadata -> setDSMetadataProperties(dsMetadata)); + } + + } + + /** + * Create metadata . + * + * @param metadataKeyName + */ + public void createMetadata(final String metadataKeyName){ + final IndexedContainer metadataContainer = (IndexedContainer) getContainerDataSource(); + final Item item = metadataContainer.addItem(metadataKeyName); + item.getItemProperty(METADATA_KEY).setValue(metadataKeyName); + } + + /** + * Delete metadata. + * + * @param metadataKeyName + */ + public void deleteMetadata(final String metadataKeyName){ + final IndexedContainer metadataContainer = (IndexedContainer) getContainerDataSource(); + metadataContainer.removeItem(metadataKeyName); + } + + private void createDSMetadataTable() { + addStyleName(ValoTheme.TABLE_NO_HORIZONTAL_LINES); + addStyleName(ValoTheme.TABLE_NO_STRIPES); + addStyleName(SPUIStyleDefinitions.SW_MODULE_TABLE); + addStyleName("details-layout"); + setSelectable(false); + setImmediate(true); + setContainerDataSource(getDistSetContainer()); + setColumnHeaderMode(ColumnHeaderMode.EXPLICIT); + addDSMetadataTableHeader(); + setSizeFull(); + //same as height of other tabs in details tabsheet + setHeight(116,Unit.PIXELS); + } + + private IndexedContainer getDistSetContainer() { + final IndexedContainer container = new IndexedContainer(); + container.addContainerProperty(METADATA_KEY, String.class, ""); + setColumnExpandRatio(METADATA_KEY, 0.7f); + setColumnAlignment(METADATA_KEY, Align.LEFT); + + if (permissionChecker.hasUpdateDistributionPermission()) { + container.addContainerProperty(VIEW, Label.class, ""); + setColumnExpandRatio(VIEW, 0.2F); + setColumnAlignment(VIEW, Align.RIGHT); + } + return container; + } + + private void addDSMetadataTableHeader() { + setColumnHeader(METADATA_KEY, i18n.get("header.key")); + } + + + private void setDSMetadataProperties(final DistributionSetMetadata dsMetadata){ + final Item item = getContainerDataSource().addItem(dsMetadata.getKey()); + item.getItemProperty(METADATA_KEY).setValue(dsMetadata.getKey()); + + } + + private void addCustomGeneratedColumns() { + addGeneratedColumn(METADATA_KEY, + (source, itemId, columnId) -> customMetadataDetailButton((String) itemId)); + } + + private Button customMetadataDetailButton(final String metadataKey) { + final Button viewIcon = SPUIComponentProvider.getButton(getDetailLinkId(metadataKey), metadataKey, "View " + + metadataKey + " Metadata details", null, false, null, SPUIButtonStyleSmallNoBorder.class); + viewIcon.setData(metadataKey); + viewIcon.addStyleName(ValoTheme.BUTTON_TINY + " " + ValoTheme.BUTTON_LINK + " " + "on-focus-no-border link" + + " " + "text-style"); + viewIcon.addClickListener(event -> showMetadataDetails(selectedDistSetId, metadataKey)); + return viewIcon; + } + + private static String getDetailLinkId(final String name) { + return new StringBuilder(SPUIComponentIdProvider.DS_METADATA_DETAIL_LINK).append('.').append(name) + .toString(); + } + + private void showMetadataDetails(final Long selectedDistSetId , final String metadataKey) { + DistributionSet distSet = distributionSetManagement.findDistributionSetById(selectedDistSetId); + + /* display the window */ + UI.getCurrent().addWindow(dsMetadataPopupLayout.getWindow(distSet, + entityFactory.generateDistributionSetMetadata(distSet, metadataKey, "") )); + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/detailslayout/SoftwareModuleMetadatadetailslayout.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/detailslayout/SoftwareModuleMetadatadetailslayout.java new file mode 100644 index 000000000..854347c11 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/detailslayout/SoftwareModuleMetadatadetailslayout.java @@ -0,0 +1,170 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.ui.common.detailslayout; + +import java.util.List; + +import org.eclipse.hawkbit.repository.EntityFactory; +import org.eclipse.hawkbit.repository.SoftwareManagement; +import org.eclipse.hawkbit.repository.SpPermissionChecker; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata; +import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; +import org.eclipse.hawkbit.ui.decorators.SPUIButtonStyleSmallNoBorder; +import org.eclipse.hawkbit.ui.distributions.smtable.SwMetadataPopupLayout; +import org.eclipse.hawkbit.ui.utils.I18N; +import org.eclipse.hawkbit.ui.utils.SPUIComponentIdProvider; +import org.eclipse.hawkbit.ui.utils.SPUIStyleDefinitions; + +import com.vaadin.data.Item; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.spring.annotation.SpringComponent; +import com.vaadin.spring.annotation.ViewScope; +import com.vaadin.ui.Button; +import com.vaadin.ui.Table; +import com.vaadin.ui.UI; +import com.vaadin.ui.themes.ValoTheme; + +/** + * + * SoftwareModule Metadata details layout. + * + */ + +@SpringComponent +@ViewScope +public class SoftwareModuleMetadatadetailslayout extends Table { + + private static final long serialVersionUID = 2913758299611838818L; + + private static final String METADATA_KEY = "Key"; + + private SpPermissionChecker permissionChecker; + + private SoftwareManagement softwareManagement; + + private SwMetadataPopupLayout swMetadataPopupLayout; + + private I18N i18n; + + private Long selectedSWModuleId; + + private transient EntityFactory entityFactory; + + public void init(final I18N i18n, final SpPermissionChecker permissionChecker, + final SoftwareManagement softwareManagement, final SwMetadataPopupLayout swMetadataPopupLayout, + final EntityFactory entityFactory) { + this.i18n = i18n; + this.permissionChecker = permissionChecker; + this.softwareManagement = softwareManagement; + this.swMetadataPopupLayout = swMetadataPopupLayout; + this.entityFactory = entityFactory; + createSWMMetadataTable(); + addCustomGeneratedColumns(); + } + + /** + * Populate software module metadata table. + * + * @param swModule + */ + public void populateSMMetadata(final SoftwareModule swModule) { + removeAllItems(); + if (null == swModule) { + return; + } + selectedSWModuleId = swModule.getId(); + final List swMetadataList = swModule.getMetadata(); + if (null != swMetadataList && !swMetadataList.isEmpty()) { + swMetadataList.forEach(swMetadata -> setSWMetadataProperties(swMetadata)); + } + } + + /** + * Create metadata. + * + * @param metadataKeyName + */ + public void createMetadata(final String metadataKeyName) { + final IndexedContainer metadataContainer = (IndexedContainer) getContainerDataSource(); + final Item item = metadataContainer.addItem(metadataKeyName); + item.getItemProperty(METADATA_KEY).setValue(metadataKeyName); + + } + + /** + * Delete metadata. + * + * @param metadataKeyName + */ + public void deleteMetadata(final String metadataKeyName) { + final IndexedContainer metadataContainer = (IndexedContainer) getContainerDataSource(); + metadataContainer.removeItem(metadataKeyName); + } + + private void createSWMMetadataTable() { + addStyleName(ValoTheme.TABLE_NO_HORIZONTAL_LINES); + addStyleName(ValoTheme.TABLE_NO_STRIPES); + addStyleName(SPUIStyleDefinitions.SW_MODULE_TABLE); + setSelectable(false); + setImmediate(true); + setContainerDataSource(getSwModuleMetadataContainer()); + setColumnHeaderMode(ColumnHeaderMode.EXPLICIT); + addSMMetadataTableHeader(); + setSizeFull(); + //same as height of other tabs in details tabsheet + setHeight(116,Unit.PIXELS); + } + + private IndexedContainer getSwModuleMetadataContainer() { + final IndexedContainer container = new IndexedContainer(); + container.addContainerProperty(METADATA_KEY, String.class, ""); + setColumnAlignment(METADATA_KEY, Align.LEFT); + return container; + } + + private void addSMMetadataTableHeader() { + setColumnHeader(METADATA_KEY, i18n.get("header.key")); + } + + + private void setSWMetadataProperties(final SoftwareModuleMetadata swMetadata) { + final Item item = getContainerDataSource().addItem(swMetadata.getKey()); + item.getItemProperty(METADATA_KEY).setValue(swMetadata.getKey()); + } + + private void addCustomGeneratedColumns() { + addGeneratedColumn(METADATA_KEY, (source, itemId, columnId) -> customMetadataDetailButton((String) itemId)); + } + + private Button customMetadataDetailButton(final String metadataKey) { + final Button viewLink = SPUIComponentProvider.getButton(getDetailLinkId(metadataKey), metadataKey, "View" + + metadataKey + " Metadata details", null, false, null, SPUIButtonStyleSmallNoBorder.class); + viewLink.setData(metadataKey); + if (permissionChecker.hasUpdateDistributionPermission()) { + viewLink.addStyleName(ValoTheme.BUTTON_TINY + " " + ValoTheme.BUTTON_LINK + " " + "on-focus-no-border link" + + " " + "text-style"); + viewLink.addClickListener(event -> showMetadataDetails(selectedSWModuleId, metadataKey)); + } + return viewLink; + } + + private static String getDetailLinkId(final String name) { + return new StringBuilder(SPUIComponentIdProvider.SW_METADATA_DETAIL_LINK).append('.').append(name).toString(); + } + + private void showMetadataDetails(final Long selectedSWModuleId, final String metadataKey) { + SoftwareModule swmodule = softwareManagement.findSoftwareModuleById(selectedSWModuleId); + /* display the window */ + UI.getCurrent().addWindow( + swMetadataPopupLayout.getWindow(swmodule, + entityFactory.generateSoftwareModuleMetadata(swmodule, metadataKey, ""))); + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/filterlayout/AbstractFilterMultiButtonClick.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/filterlayout/AbstractFilterMultiButtonClick.java index 8b3ac4cde..f7d3656b1 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/filterlayout/AbstractFilterMultiButtonClick.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/common/filterlayout/AbstractFilterMultiButtonClick.java @@ -22,7 +22,8 @@ import com.vaadin.ui.Button.ClickEvent; */ public abstract class AbstractFilterMultiButtonClick extends AbstractFilterButtonClickBehaviour { - protected final Set