diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java index 3456d7979..4f83ef160 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/exception/SpServerError.java @@ -17,74 +17,96 @@ public enum SpServerError { * */ SP_REPO_GENERIC_ERROR("hawkbit.server.error.repo.genericError", "unknown error occured"), + /** * */ - SP_REPO_ENTITY_ALRREADY_EXISTS("hawkbit.server.error.repo.entitiyAlreayExists", "The given entity already exists in database"), + SP_REPO_ENTITY_ALRREADY_EXISTS("hawkbit.server.error.repo.entitiyAlreayExists", + "The given entity already exists in database"), + /** * */ - SP_REPO_CONSTRAINT_VIOLATION("hawkbit.server.error.repo.constraintViolation", "The given entity cannot be saved due to Constraint Violation"), + SP_REPO_CONSTRAINT_VIOLATION("hawkbit.server.error.repo.constraintViolation", + "The given entity cannot be saved due to Constraint Violation"), + /** * */ - SP_REPO_INVALID_TARGET_ADDRESS("hawkbit.server.error.repo.invalidTargetAddress", "The target address is not well formed"), + SP_REPO_INVALID_TARGET_ADDRESS("hawkbit.server.error.repo.invalidTargetAddress", + "The target address is not well formed"), + /** - * - */ - SP_REPO_ENTITY_NOT_EXISTS("hawkbit.server.error.repo.entitiyNotFound", "The given entity does not exist in the repository"), + * + */ + SP_REPO_ENTITY_NOT_EXISTS("hawkbit.server.error.repo.entitiyNotFound", + "The given entity does not exist in the repository"), + /** - * - */ - SP_REPO_CONCURRENT_MODIFICATION("hawkbit.server.error.repo.concurrentModification", "The given entity has been changed by another user/session"), + * + */ + SP_REPO_CONCURRENT_MODIFICATION("hawkbit.server.error.repo.concurrentModification", + "The given entity has been changed by another user/session"), + /** - * - */ - SP_REST_SORT_PARAM_SYNTAX("hawkbit.server.error.rest.param.sortParamSyntax", "The given sort paramter is not well formed"), + * + */ + SP_REST_SORT_PARAM_SYNTAX("hawkbit.server.error.rest.param.sortParamSyntax", + "The given sort paramter is not well formed"), + + /** + * + */ + SP_REST_RSQL_SEARCH_PARAM_SYNTAX("hawkbit.server.error.rest.param.rsqlParamSyntax", + "The given search paramter is not well formed"), + + /** + * + */ + SP_REST_RSQL_PARAM_INVALID_FIELD("hawkbit.server.error.rest.param.rsqlInvalidField", + "The given search parameter field does not exist"), + + /** + * + */ + SP_REST_SORT_PARAM_INVALID_FIELD("hawkbit.server.error.rest.param.invalidField", + "The given sort parameter field does not exist"), + + /** + * + */ + SP_REST_SORT_PARAM_INVALID_DIRECTION("hawkbit.server.error.rest.param.invalidDirection", + "The given sort parameter direction does not exist"), + + /** + * + */ + SP_REST_BODY_NOT_READABLE("hawkbit.server.error.rest.body.notReadable", + "The given request body is not well formed"), + + /** + * + */ + SP_ARTIFACT_UPLOAD_FAILED("hawkbit.server.error.artifact.uploadFailed", + "Upload of artifact failed with internal server error."), /** * */ - SP_REST_RSQL_SEARCH_PARAM_SYNTAX("hawkbit.server.error.rest.param.rsqlParamSyntax", "The given search paramter is not well formed"), + SP_ARTIFACT_UPLOAD_FAILED_MD5_MATCH("hawkbit.server.error.artifact.uploadFailed.checksum.md5.match", + "Upload of artifact failed as the provided MD5 checksum did not match with the provided artifact."), /** * */ - SP_REST_RSQL_PARAM_INVALID_FIELD("hawkbit.server.error.rest.param.rsqlInvalidField", "The given search parameter field does not exist"), + SP_ARTIFACT_UPLOAD_FAILED_SHA1_MATCH("hawkbit.server.error.artifact.uploadFailed.checksum.sha1.match", + "Upload of artifact failed as the provided SHA1 checksum did not match with the provided artifact."), /** * */ - SP_REST_SORT_PARAM_INVALID_FIELD("hawkbit.server.error.rest.param.invalidField", "The given sort parameter field does not exist"), - - /** - * - */ - SP_REST_SORT_PARAM_INVALID_DIRECTION("hawkbit.server.error.rest.param.invalidDirection", "The given sort parameter direction does not exist"), - - /** - * - */ - SP_REST_BODY_NOT_READABLE("hawkbit.server.error.rest.body.notReadable", "The given request body is not well formed"), - /** - * - */ - SP_ARTIFACT_UPLOAD_FAILED("hawkbit.server.error.artifact.uploadFailed", "Upload of artifact failed with internal server error."), - - /** - * - */ - SP_ARTIFACT_UPLOAD_FAILED_MD5_MATCH("hawkbit.server.error.artifact.uploadFailed.checksum.md5.match", "Upload of artifact failed as the provided MD5 checksum did not match with the provided artifact."), - - /** - * - */ - SP_ARTIFACT_UPLOAD_FAILED_SHA1_MATCH("hawkbit.server.error.artifact.uploadFailed.checksum.sha1.match", "Upload of artifact failed as the provided SHA1 checksum did not match with the provided artifact."), - - /** - * - */ - SP_DS_CREATION_FAILED_MISSING_MODULE("hawkbit.server.error.distributionset.creationFailed.missingModule", "Creation if Distribution Set failed as module is missing that is configured as mandatory."), + SP_DS_CREATION_FAILED_MISSING_MODULE("hawkbit.server.error.distributionset.creationFailed.missingModule", + "Creation if Distribution Set failed as module is missing that is configured as mandatory."), /** * @@ -94,63 +116,74 @@ public enum SpServerError { /** * */ - SP_ARTIFACT_DELETE_FAILED("hawkbit.server.error.artifact.deleteFailed", "Deletion of artifact failed with internal server error."), - /** - * - */ - SP_ARTIFACT_LOAD_FAILED("hawkbit.server.error.artifact.loadFailed", "Load of artifact failed with internal server error."), + SP_ARTIFACT_DELETE_FAILED("hawkbit.server.error.artifact.deleteFailed", + "Deletion of artifact failed with internal server error."), /** - * - */ + * + */ + SP_ARTIFACT_LOAD_FAILED("hawkbit.server.error.artifact.loadFailed", + "Load of artifact failed with internal server error."), + + /** + * + */ SP_QUOTA_EXCEEDED("hawkbit.server.error.quota.tooManyEntries", "Too many entries have been inserted."), /** * error message, which describes that the action can not be canceled cause * the action is inactive. */ - SP_ACTION_NOT_CANCELABLE("hawkbit.server.error.action.notcancelable", "Only active actions which are in status pending are canceable."), + SP_ACTION_NOT_CANCELABLE("hawkbit.server.error.action.notcancelable", + "Only active actions which are in status pending are canceable."), /** * error message, which describes that the action can not be force quit * cause the action is inactive. */ - SP_ACTION_NOT_FORCE_QUITABLE("hawkbit.server.error.action.notforcequitable", "Only active actions which are in status pending can be force quit."), + SP_ACTION_NOT_FORCE_QUITABLE("hawkbit.server.error.action.notforcequitable", + "Only active actions which are in status pending can be force quit."), + + /** + * + */ + SP_DS_INCOMPLETE("hawkbit.server.error.distributionset.incomplete", + "Distribution set is assigned to a a target that is incomplete (i.e. mandatory modules are missing)"), + + /** + * + */ + SP_DS_TYPE_UNDEFINED("hawkbit.server.error.distributionset.type.undefined", + "Distribution set type is not yet defined. Modules cannot be added until definition."), /** * */ - SP_DS_INCOMPLETE("hawkbit.server.error.distributionset.incomplete", "Distribution set is assigned to a a target that is incomplete (i.e. mandatory modules are missing)"), + SP_DS_MODULE_UNSUPPORTED("hawkbit.server.error.distributionset.modules.unsupported", + "Distribution set type does not contain the given module, i.e. is incompatible."), /** - * - */ - SP_DS_TYPE_UNDEFINED("hawkbit.server.error.distributionset.type.undefined", "Distribution set type is not yet defined. Modules cannot be added until definition."), + * + */ + SP_REPO_TENANT_NOT_EXISTS("hawkbit.server.error.repo.tenantNotExists", + "The entity cannot be inserted due the tenant does not exists"), /** - * - */ - SP_DS_MODULE_UNSUPPORTED("hawkbit.server.error.distributionset.modules.unsupported", "Distribution set type does not contain the given module, i.e. is incompatible."), - - /** - * - */ - SP_REPO_TENANT_NOT_EXISTS("hawkbit.server.error.repo.tenantNotExists", "The entity cannot be inserted due the tenant does not exists"), - - /** - * - */ + * + */ SP_ENTITY_LOCKED("hawkbit.server.error.entitiylocked", "The given entity is locked by the server."), /** - * - */ - SP_REPO_ENTITY_READ_ONLY("hawkbit.server.error.entityreadonly", "The given entity is read only and the change cannot be completed."), + * + */ + SP_REPO_ENTITY_READ_ONLY("hawkbit.server.error.entityreadonly", + "The given entity is read only and the change cannot be completed."), /** * */ - SP_CONFIGURATION_VALUE_INVALID("hawkbit.server.error.configValueInvalid", "The given configuration value is invalid."), + SP_CONFIGURATION_VALUE_INVALID("hawkbit.server.error.configValueInvalid", + "The given configuration value is invalid."), /** * */ @@ -159,22 +192,26 @@ public enum SpServerError { /** * */ - SP_ROLLOUT_ILLEGAL_STATE("hawkbit.server.error.rollout.illegalstate", "The rollout is in the wrong state for the requested operation"), + SP_ROLLOUT_ILLEGAL_STATE("hawkbit.server.error.rollout.illegalstate", + "The rollout is in the wrong state for the requested operation"), /** * */ - SP_ROLLOUT_VERIFICATION_FAILED("hawkbit.server.error.rollout.verificationFailed", "The rollout configuration could not be verified successfully"), + SP_ROLLOUT_VERIFICATION_FAILED("hawkbit.server.error.rollout.verificationFailed", + "The rollout configuration could not be verified successfully"), /** - * - */ - SP_REPO_OPERATION_NOT_SUPPORTED("hawkbit.server.error.operation.notSupported", "Operation or method is (no longer) supported by service."), + * + */ + SP_REPO_OPERATION_NOT_SUPPORTED("hawkbit.server.error.operation.notSupported", + "Operation or method is (no longer) supported by service."), /** * Error message informing that the maintenance schedule is invalid. */ - SP_MAINTENANCE_SCHEDULE_INVALID("hawkbit.server.error.maintenanceScheduleInvalid", "Information for schedule, duration or timezone is missing; or there is no valid maintenance window available in future."); + SP_MAINTENANCE_SCHEDULE_INVALID("hawkbit.server.error.maintenanceScheduleInvalid", + "Information for schedule, duration or timezone is missing; or there is no valid maintenance window available in future."); private final String key; private final String message; diff --git a/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiConfigData.java b/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiConfigData.java index 4cd140ca7..4e3a0421d 100644 --- a/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiConfigData.java +++ b/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiConfigData.java @@ -23,6 +23,8 @@ public class DdiConfigData extends DdiActionFeedback { @NotEmpty private final Map data; + private final DdiUpdateMode mode; + /** * Constructor. * @@ -38,18 +40,24 @@ public class DdiConfigData extends DdiActionFeedback { @JsonCreator public DdiConfigData(@JsonProperty(value = "id") final Long id, @JsonProperty(value = "time") final String time, @JsonProperty(value = "status") final DdiStatus status, - @JsonProperty(value = "data") final Map data) { + @JsonProperty(value = "data") final Map data, + @JsonProperty(value = "mode") final DdiUpdateMode mode) { super(id, time, status); this.data = data; + this.mode = mode; } public Map getData() { return data; } + public DdiUpdateMode getMode() { + return mode; + } + @Override public String toString() { - return "ConfigData [data=" + data + ", toString()=" + super.toString() + "]"; + return "ConfigData [data=" + data + ", mode=" + mode + ", toString()=" + super.toString() + "]"; } } diff --git a/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiUpdateMode.java b/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiUpdateMode.java new file mode 100644 index 000000000..0c2c0efc8 --- /dev/null +++ b/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/json/model/DdiUpdateMode.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2018 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.ddi.json.model; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Enumerates the supported update modes. Each mode represents an attribute + * update strategy. + * + * @see DdiConfigData + */ +public enum DdiUpdateMode { + + /** + * Merge update strategy + */ + MERGE("merge"), + + /** + * Replacement update strategy + */ + REPLACE("replace"), + + /** + * Removal update strategy + */ + REMOVE("remove"); + + private String name; + + DdiUpdateMode(final String name) { + this.name = name; + } + + @JsonValue + public String getName() { + return name; + } + +} 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 f83755f68..1640af907 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 @@ -32,6 +32,7 @@ import org.eclipse.hawkbit.ddi.json.model.DdiDeployment.HandlingType; import org.eclipse.hawkbit.ddi.json.model.DdiDeployment.DdiMaintenanceWindowStatus; import org.eclipse.hawkbit.ddi.json.model.DdiDeploymentBase; import org.eclipse.hawkbit.ddi.json.model.DdiResult.FinalResult; +import org.eclipse.hawkbit.ddi.json.model.DdiUpdateMode; import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants; import org.eclipse.hawkbit.ddi.rest.api.DdiRootControllerRestApi; import org.eclipse.hawkbit.repository.ArtifactManagement; @@ -39,6 +40,7 @@ import org.eclipse.hawkbit.repository.ControllerManagement; import org.eclipse.hawkbit.repository.EntityFactory; import org.eclipse.hawkbit.repository.RepositoryConstants; import org.eclipse.hawkbit.repository.SystemManagement; +import org.eclipse.hawkbit.repository.UpdateMode; import org.eclipse.hawkbit.repository.builder.ActionStatusCreate; import org.eclipse.hawkbit.repository.event.remote.DownloadProgressEvent; import org.eclipse.hawkbit.repository.exception.ArtifactBinaryNotFoundException; @@ -440,8 +442,8 @@ public class DdiRootController implements DdiRootControllerRestApi { @Override public ResponseEntity putConfigData(@Valid @RequestBody final DdiConfigData configData, @PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId) { - controllerManagement.updateControllerAttributes(controllerId, configData.getData()); + controllerManagement.updateControllerAttributes(controllerId, configData.getData(), getUpdateMode(configData)); return ResponseEntity.ok().build(); } @@ -579,4 +581,16 @@ public class DdiRootController implements DdiRootControllerRestApi { } } } + + /** + * Retrieve the update mode from the given update message. + */ + private static UpdateMode getUpdateMode(final DdiConfigData configData) { + final DdiUpdateMode mode = configData.getMode(); + if (mode != null) { + return UpdateMode.valueOf(mode.name()); + } + return null; + } + } diff --git a/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiConfigDataTest.java b/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiConfigDataTest.java index 4baf38ec7..2033f07ae 100644 --- a/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiConfigDataTest.java +++ b/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiConfigDataTest.java @@ -33,6 +33,7 @@ import com.google.common.collect.Maps; import ru.yandex.qatools.allure.annotations.Description; import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Step; import ru.yandex.qatools.allure.annotations.Stories; /** @@ -46,6 +47,7 @@ public class DdiConfigDataTest extends AbstractDDiApiIntegrationTest { @Test @Description("We verify that the config data (i.e. device attributes like serial number, hardware revision etc.) " + "are requested only once from the device.") + @SuppressWarnings("squid:S2925") public void requestConfigDataIfEmpty() throws Exception { final Target savedTarget = testdataFactory.createTarget("4712"); @@ -69,10 +71,9 @@ public class DdiConfigDataTest extends AbstractDDiApiIntegrationTest { attributes.put("dsafsdf", "sdsds"); final Target updateControllerAttributes = controllerManagement - .updateControllerAttributes(savedTarget.getControllerId(), attributes); + .updateControllerAttributes(savedTarget.getControllerId(), attributes, null); // request controller attributes need to be false because we don't want - // to request the - // controller attributes again + // to request the controller attributes again assertThat(updateControllerAttributes.isRequestControllerAttributes()).isFalse(); mvc.perform( @@ -133,7 +134,7 @@ public class DdiConfigDataTest extends AbstractDDiApiIntegrationTest { @Description("We verify that the config data (i.e. device attributes like serial number, hardware revision etc.) " + "resource behaves as exptected in cae of invalid request attempts.") public void badConfigData() throws Exception { - final Target savedTarget = testdataFactory.createTarget("4712"); + testdataFactory.createTarget("4712"); // not allowed methods mvc.perform(post("/{tenant}/controller/v1/4712/configData", tenantAware.getCurrentTenant())) @@ -163,4 +164,143 @@ public class DdiConfigDataTest extends AbstractDDiApiIntegrationTest { .content("{\"id\": \"51659181\"}").contentType(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultPrinter.print()).andExpect(status().isBadRequest()); } + + @Test + @Description("Verify that config data (device attributes) can be updated by the controller using different update modes (merge, replace, remove).") + public void putConfigDataWithDifferentUpdateModes() throws Exception { + + // create a target + final String controllerId = "4717"; + testdataFactory.createTarget(controllerId); + final String configDataPath = "/{tenant}/controller/v1/" + controllerId + "/configData"; + + // no update mode + putConfigDataWithoutUpdateMode(controllerId, configDataPath); + + // update mode REPLACE + putConfigDataWithUpdateModeReplace(controllerId, configDataPath); + + // update mode MERGE + putConfigDataWithUpdateModeMerge(controllerId, configDataPath); + + // update mode REMOVE + putConfigDataWithUpdateModeRemove(controllerId, configDataPath); + + // invalid update mode + putConfigDataWithInvalidUpdateMode(configDataPath); + + } + + @Step + private void putConfigDataWithInvalidUpdateMode(final String configDataPath) + throws Exception { + + // create some attriutes + final Map attributes = new HashMap<>(); + attributes.put("k0", "v0"); + attributes.put("k1", "v1"); + + // use an invalid update mode + mvc.perform(put(configDataPath, tenantAware.getCurrentTenant()) + .content(JsonBuilder.configData("", attributes, "closed", "KJHGKJHGKJHG")) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isBadRequest()); + } + + @Step + private void putConfigDataWithUpdateModeRemove(final String controllerId, final String configDataPath) + throws Exception { + + // get the current attributes + final int previousSize = targetManagement.getControllerAttributes(controllerId).size(); + + // update the attributes using update mode REMOVE + final Map removeAttributes = new HashMap<>(); + removeAttributes.put("k1", "foo"); + removeAttributes.put("k3", "bar"); + + mvc.perform(put(configDataPath, tenantAware.getCurrentTenant()) + .content(JsonBuilder.configData("", removeAttributes, "closed", "remove")) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + + // verify attribute removal + final Map updatedAttributes = targetManagement.getControllerAttributes(controllerId); + assertThat(updatedAttributes.size()).isEqualTo(previousSize - 2); + assertThat(updatedAttributes).doesNotContainKeys("k1", "k3"); + + } + + @Step + private void putConfigDataWithUpdateModeMerge(final String controllerId, final String configDataPath) + throws Exception { + + // get the current attributes + final Map attributes = new HashMap<>(targetManagement.getControllerAttributes(controllerId)); + + // update the attributes using update mode MERGE + final Map mergeAttributes = new HashMap<>(); + mergeAttributes.put("k1", "v1_modified_again"); + mergeAttributes.put("k4", "v4"); + mvc.perform(put(configDataPath, tenantAware.getCurrentTenant()) + .content(JsonBuilder.configData("", mergeAttributes, "closed", "merge")) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + + // verify attribute merge + final Map updatedAttributes = targetManagement.getControllerAttributes(controllerId); + assertThat(updatedAttributes.size()).isEqualTo(4); + assertThat(updatedAttributes).containsAllEntriesOf(mergeAttributes); + assertThat(updatedAttributes.get("k1")).isEqualTo("v1_modified_again"); + attributes.keySet().forEach(assertThat(updatedAttributes)::containsKey); + + } + + @Step + private void putConfigDataWithUpdateModeReplace(final String controllerId, final String configDataPath) + throws Exception { + + // get the current attributes + final Map attributes = new HashMap<>(targetManagement.getControllerAttributes(controllerId)); + + // update the attributes using update mode REPLACE + final Map replacementAttributes = new HashMap<>(); + replacementAttributes.put("k1", "v1_modified"); + replacementAttributes.put("k2", "v2"); + replacementAttributes.put("k3", "v3"); + mvc.perform(put(configDataPath, tenantAware.getCurrentTenant()) + .content(JsonBuilder.configData("", replacementAttributes, "closed", "replace")) + .contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print()) + .andExpect(status().isOk()); + + // verify attribute replacement + final Map updatedAttributes = targetManagement.getControllerAttributes(controllerId); + assertThat(updatedAttributes.size()).isEqualTo(replacementAttributes.size()); + assertThat(updatedAttributes).containsAllEntriesOf(replacementAttributes); + assertThat(updatedAttributes.get("k1")).isEqualTo("v1_modified"); + attributes.entrySet().forEach(assertThat(updatedAttributes)::doesNotContain); + + } + + @Step + private void putConfigDataWithoutUpdateMode(final String controllerId, final String configDataPath) + throws Exception { + + // create some attriutes + final Map attributes = new HashMap<>(); + attributes.put("k0", "v0"); + attributes.put("k1", "v1"); + + // set the initial attributes + mvc.perform(put(configDataPath, tenantAware.getCurrentTenant()) + .content(JsonBuilder.configData("", attributes, "closed")).contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk()); + + // verify the initial parameters + final Map updatedAttributes = targetManagement.getControllerAttributes(controllerId); + assertThat(updatedAttributes.size()).isEqualTo(attributes.size()); + assertThat(updatedAttributes).containsAllEntriesOf(attributes); + + } + } diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java index 1a35a578a..93ff80958 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java @@ -22,11 +22,13 @@ import org.eclipse.hawkbit.dmf.amqp.api.MessageHeaderKey; import org.eclipse.hawkbit.dmf.amqp.api.MessageType; import org.eclipse.hawkbit.dmf.json.model.DmfActionUpdateStatus; import org.eclipse.hawkbit.dmf.json.model.DmfAttributeUpdate; +import org.eclipse.hawkbit.dmf.json.model.DmfUpdateMode; import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails; import org.eclipse.hawkbit.repository.ControllerManagement; import org.eclipse.hawkbit.repository.EntityFactory; import org.eclipse.hawkbit.repository.RepositoryConstants; +import org.eclipse.hawkbit.repository.UpdateMode; import org.eclipse.hawkbit.repository.builder.ActionStatusCreate; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Action.Status; @@ -248,8 +250,8 @@ public class AmqpMessageHandlerService extends BaseAmqpService { private void updateAttributes(final Message message) { final DmfAttributeUpdate attributeUpdate = convertMessage(message, DmfAttributeUpdate.class); final String thingId = getStringHeaderKey(message, MessageHeaderKey.THING_ID, "ThingId is null"); - - controllerManagement.updateControllerAttributes(thingId, attributeUpdate.getAttributes()); + controllerManagement.updateControllerAttributes(thingId, attributeUpdate.getAttributes(), + getUpdateMode(attributeUpdate)); } /** @@ -364,4 +366,16 @@ public class AmqpMessageHandlerService extends BaseAmqpService { return findActionWithDetails.get(); } + + /** + * Retrieve the update mode from the given update message. + */ + private static UpdateMode getUpdateMode(final DmfAttributeUpdate update) { + final DmfUpdateMode mode = update.getMode(); + if (mode != null) { + return UpdateMode.valueOf(mode.name()); + } + return null; + } + } diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java index db2fad339..8379c279b 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerServiceTest.java @@ -36,9 +36,11 @@ import org.eclipse.hawkbit.dmf.json.model.DmfActionStatus; import org.eclipse.hawkbit.dmf.json.model.DmfActionUpdateStatus; import org.eclipse.hawkbit.dmf.json.model.DmfAttributeUpdate; import org.eclipse.hawkbit.dmf.json.model.DmfDownloadResponse; +import org.eclipse.hawkbit.dmf.json.model.DmfUpdateMode; import org.eclipse.hawkbit.repository.ArtifactManagement; import org.eclipse.hawkbit.repository.ControllerManagement; import org.eclipse.hawkbit.repository.EntityFactory; +import org.eclipse.hawkbit.repository.UpdateMode; import org.eclipse.hawkbit.repository.builder.ActionStatusBuilder; import org.eclipse.hawkbit.repository.builder.ActionStatusCreate; import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; @@ -123,6 +125,9 @@ public class AmqpMessageHandlerServiceTest { @Captor private ArgumentCaptor targetIdCaptor; + @Captor + private ArgumentCaptor modeCaptor; + @Before public void before() throws Exception { messageConverter = new Jackson2JsonMessageConverter(); @@ -181,7 +186,7 @@ public class AmqpMessageHandlerServiceTest { public void updateAttributes() { final String knownThingId = "1"; final MessageProperties messageProperties = createMessageProperties(MessageType.EVENT); - messageProperties.setHeader(MessageHeaderKey.THING_ID, "1"); + messageProperties.setHeader(MessageHeaderKey.THING_ID, knownThingId); messageProperties.setHeader(MessageHeaderKey.TOPIC, "UPDATE_ATTRIBUTES"); final DmfAttributeUpdate attributeUpdate = new DmfAttributeUpdate(); attributeUpdate.getAttributes().put("testKey1", "testValue1"); @@ -190,8 +195,8 @@ public class AmqpMessageHandlerServiceTest { final Message message = amqpMessageHandlerService.getMessageConverter().toMessage(attributeUpdate, messageProperties); - when(controllerManagementMock.updateControllerAttributes(targetIdCaptor.capture(), attributesCaptor.capture())) - .thenReturn(null); + when(controllerManagementMock.updateControllerAttributes(targetIdCaptor.capture(), attributesCaptor.capture(), + modeCaptor.capture())).thenReturn(null); amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, "vHost"); @@ -202,6 +207,50 @@ public class AmqpMessageHandlerServiceTest { } + @Test + @Description("Verifies that the update mode is retrieved from the UPDATE_ATTRIBUTES message and passed to the controller management.") + public void attributeUpdateModes() { + final String knownThingId = "1"; + final MessageProperties messageProperties = createMessageProperties(MessageType.EVENT); + messageProperties.setHeader(MessageHeaderKey.THING_ID, knownThingId); + messageProperties.setHeader(MessageHeaderKey.TOPIC, "UPDATE_ATTRIBUTES"); + final DmfAttributeUpdate attributeUpdate = new DmfAttributeUpdate(); + attributeUpdate.getAttributes().put("testKey1", "testValue1"); + attributeUpdate.getAttributes().put("testKey2", "testValue2"); + + when(controllerManagementMock.updateControllerAttributes(targetIdCaptor.capture(), attributesCaptor.capture(), + modeCaptor.capture())).thenReturn(null); + + // send a message which does not specify a update mode + Message message = amqpMessageHandlerService.getMessageConverter().toMessage(attributeUpdate, messageProperties); + amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, "vHost"); + // verify that NO fallback is made on the way to the controller + // management layer + assertThat(modeCaptor.getValue()).isNull(); + + // send a message which specifies update mode MERGE + attributeUpdate.setMode(DmfUpdateMode.MERGE); + message = amqpMessageHandlerService.getMessageConverter().toMessage(attributeUpdate, messageProperties); + amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, "vHost"); + // verify that the update mode is converted and forwarded as expected + assertThat(modeCaptor.getValue()).isEqualTo(UpdateMode.MERGE); + + // send a message which specifies update mode REPLACE + attributeUpdate.setMode(DmfUpdateMode.REPLACE); + message = amqpMessageHandlerService.getMessageConverter().toMessage(attributeUpdate, messageProperties); + amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, "vHost"); + // verify that the update mode is converted and forwarded as expected + assertThat(modeCaptor.getValue()).isEqualTo(UpdateMode.REPLACE); + + // send a message which specifies update mode REMOVE + attributeUpdate.setMode(DmfUpdateMode.REMOVE); + message = amqpMessageHandlerService.getMessageConverter().toMessage(attributeUpdate, messageProperties); + amqpMessageHandlerService.onMessage(message, MessageType.EVENT.name(), TENANT, "vHost"); + // verify that the update mode is converted and forwarded as expected + assertThat(modeCaptor.getValue()).isEqualTo(UpdateMode.REMOVE); + + } + @Test @Description("Tests the creation of a thing without a 'reply to' header in message.") public void createThingWitoutReplyTo() { diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageHandlerServiceIntegrationTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageHandlerServiceIntegrationTest.java index 489068dbf..41e6887d8 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageHandlerServiceIntegrationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageHandlerServiceIntegrationTest.java @@ -10,7 +10,9 @@ package org.eclipse.hawkbit.integration; import static org.assertj.core.api.Assertions.assertThat; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.UUID; import java.util.stream.Collectors; @@ -22,6 +24,7 @@ import org.eclipse.hawkbit.dmf.amqp.api.MessageType; import org.eclipse.hawkbit.dmf.json.model.DmfActionStatus; import org.eclipse.hawkbit.dmf.json.model.DmfActionUpdateStatus; import org.eclipse.hawkbit.dmf.json.model.DmfAttributeUpdate; +import org.eclipse.hawkbit.dmf.json.model.DmfUpdateMode; import org.eclipse.hawkbit.repository.RepositoryConstants; import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; import org.eclipse.hawkbit.repository.event.remote.TargetPollEvent; @@ -49,6 +52,7 @@ import org.springframework.beans.factory.annotation.Autowired; import ru.yandex.qatools.allure.annotations.Description; import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Step; import ru.yandex.qatools.allure.annotations.Stories; @Features("Component Tests - Device Management Federation API") @@ -634,23 +638,104 @@ public class AmqpMessageHandlerServiceIntegrationTest extends AmqpServiceIntegra } @Test - @Description("Verify that sending an update controller attribute message to an existing target works.") + @Description("Verify that sending an update controller attribute message to an existing target works. Verify that different update modes (merge, replace, remove) can be used.") @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), - @Expect(type = TargetUpdatedEvent.class, count = 1), @Expect(type = TargetPollEvent.class, count = 1) }) - public void updateAttributes() { + @Expect(type = TargetUpdatedEvent.class, count = 4), @Expect(type = TargetPollEvent.class, count = 1) }) + public void updateAttributesWithDifferentUpdateModes() { final String controllerId = TARGET_PREFIX + "updateAttributes"; // setup registerAndAssertTargetWithExistingTenant(controllerId); - final DmfAttributeUpdate controllerAttribute = new DmfAttributeUpdate(); - controllerAttribute.getAttributes().put("test1", "testA"); - controllerAttribute.getAttributes().put("test2", "testB"); - // test - sendUpdateAttributeMessage(controllerId, TENANT_EXIST, controllerAttribute); + // no update mode specified + updateAttributesWithoutUpdateMode(controllerId); + + // update mode REPLACE + updateAttributesWithUpdateModeReplace(controllerId); + + // update mode REPLACE + updateAttributesWithUpdateModeMerge(controllerId); + + // update mode REMOVE + updateAttributesWithUpdateModeRemove(controllerId); + + } + + @Step + private void updateAttributesWithUpdateModeRemove(final String controllerId) { + + // assemble the expected attributes + final Map expectedAttributes = targetManagement.getControllerAttributes(controllerId); + expectedAttributes.remove("k1"); + expectedAttributes.remove("k3"); + + // send a update message with update mode + final Map removeAttributes = new HashMap<>(); + removeAttributes.put("k1", "foo"); + removeAttributes.put("k3", "bar"); + final DmfAttributeUpdate remove = new DmfAttributeUpdate(); + remove.setMode(DmfUpdateMode.REMOVE); + remove.getAttributes().putAll(removeAttributes); + sendUpdateAttributeMessage(controllerId, TENANT_EXIST, remove); // validate - assertUpdateAttributes(controllerId, controllerAttribute.getAttributes()); + assertUpdateAttributes(controllerId, expectedAttributes); + } + + @Step + private void updateAttributesWithUpdateModeMerge(final String controllerId) { + + // get the current attributes + final Map attributes = new HashMap<>(targetManagement.getControllerAttributes(controllerId)); + + // send a update message with update mode MERGE + final Map mergeAttributes = new HashMap<>(); + mergeAttributes.put("k1", "v1_modified_again"); + mergeAttributes.put("k4", "v4"); + final DmfAttributeUpdate merge = new DmfAttributeUpdate(); + merge.setMode(DmfUpdateMode.MERGE); + merge.getAttributes().putAll(mergeAttributes); + sendUpdateAttributeMessage(controllerId, TENANT_EXIST, merge); + + // validate + final Map expectedAttributes = new HashMap<>(); + expectedAttributes.putAll(attributes); + expectedAttributes.putAll(mergeAttributes); + assertUpdateAttributes(controllerId, expectedAttributes); + } + + @Step + private void updateAttributesWithUpdateModeReplace(final String controllerId) { + + // send a update message with update mode REPLACE + final Map replacementAttributes = new HashMap<>(); + replacementAttributes.put("k1", "v1_modified"); + replacementAttributes.put("k2", "v2"); + replacementAttributes.put("k3", "v3"); + final DmfAttributeUpdate replace = new DmfAttributeUpdate(); + replace.setMode(DmfUpdateMode.REPLACE); + replace.getAttributes().putAll(replacementAttributes); + sendUpdateAttributeMessage(controllerId, TENANT_EXIST, replace); + + // validate + final Map expectedAttributes = replacementAttributes; + assertUpdateAttributes(controllerId, expectedAttributes); + } + + @Step + private void updateAttributesWithoutUpdateMode(final String controllerId) { + + // send a update message which does not specify a update mode + final Map initialAttributes = new HashMap<>(); + initialAttributes.put("k0", "v0"); + initialAttributes.put("k1", "v1"); + final DmfAttributeUpdate defaultUpdate = new DmfAttributeUpdate(); + defaultUpdate.getAttributes().putAll(initialAttributes); + sendUpdateAttributeMessage(controllerId, TENANT_EXIST, defaultUpdate); + + // validate + final Map expectedAttributes = initialAttributes; + assertUpdateAttributes(controllerId, expectedAttributes); } @Test diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpServiceIntegrationTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpServiceIntegrationTest.java index 1506d9fa1..58eb8fbe5 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpServiceIntegrationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpServiceIntegrationTest.java @@ -49,6 +49,8 @@ import org.springframework.amqp.rabbit.test.RabbitListenerTestHarness; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; +import ru.yandex.qatools.allure.annotations.Step; + /** * * Common class for {@link AmqpMessageHandlerServiceIntegrationTest} and @@ -233,6 +235,7 @@ public abstract class AmqpServiceIntegrationTest extends AbstractAmqpIntegration return replyMessage; } + @Step protected void registerAndAssertTargetWithExistingTenant(final String controllerId) { registerAndAssertTargetWithExistingTenant(controllerId, 1); } @@ -326,6 +329,7 @@ public abstract class AmqpServiceIntegrationTest extends AbstractAmqpIntegration } + @Step protected void assertUpdateAttributes(final String controllerId, final Map attributes) { final Target findByControllerId = waitUntilIsPresent( () -> controllerManagement.getByControllerId(controllerId)); diff --git a/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfAttributeUpdate.java b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfAttributeUpdate.java index 1df890e07..ea52dac75 100644 --- a/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfAttributeUpdate.java +++ b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfAttributeUpdate.java @@ -8,7 +8,6 @@ */ package org.eclipse.hawkbit.dmf.json.model; -import java.lang.annotation.Target; import java.util.HashMap; import java.util.Map; @@ -18,16 +17,28 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; /** - * Map of {@link Target} attributes. - * + * JSON representation of the Attribute Update message. */ @JsonInclude(Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public class DmfAttributeUpdate { + @JsonProperty private final Map attributes = new HashMap<>(); + @JsonProperty + private DmfUpdateMode mode; + + public DmfUpdateMode getMode() { + return mode; + } + + public void setMode(final DmfUpdateMode mode) { + this.mode = mode; + } + public Map getAttributes() { return attributes; } + } diff --git a/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfUpdateMode.java b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfUpdateMode.java new file mode 100644 index 000000000..7c204f998 --- /dev/null +++ b/hawkbit-dmf/hawkbit-dmf-api/src/main/java/org/eclipse/hawkbit/dmf/json/model/DmfUpdateMode.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2018 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.dmf.json.model; + +/** + * Enumerates the supported update modes. Each mode represents an attribute + * update strategy. + * + * @see DmfAttributeUpdate + */ +public enum DmfUpdateMode { + + /** + * Merge update strategy + */ + MERGE, + + /** + * Replacement update strategy + */ + REPLACE, + + /** + * Removal update strategy + */ + REMOVE; + +} 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 ed286c939..a67876b15 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 @@ -1383,7 +1383,7 @@ public class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest knownControllerAttrs.put("a", "1"); knownControllerAttrs.put("b", "2"); testdataFactory.createTarget(knownTargetId); - controllerManagement.updateControllerAttributes(knownTargetId, knownControllerAttrs); + controllerManagement.updateControllerAttributes(knownTargetId, knownControllerAttrs, null); // test query target over rest resource mvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + knownTargetId + "/attributes")) 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 1754c686c..553d00af3 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 @@ -319,12 +319,15 @@ public interface ControllerManagement { Action registerRetrieved(long actionId, String message); /** - * Updates attributes of the controller. + * Updates attributes of the controller according to the given + * {@link UpdateMode}. * * @param controllerId * to update * @param attributes * to insert + * @param mode + * the update mode or null * * @return updated {@link Target} * @@ -334,7 +337,8 @@ public interface ControllerManagement { * if maximum number of attribzes per target is exceeded */ @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) - Target updateControllerAttributes(@NotEmpty String controllerId, @NotNull Map attributes); + Target updateControllerAttributes(@NotEmpty String controllerId, @NotNull Map attributes, + UpdateMode mode); /** * Finds {@link Target} based on given controller ID returns found Target diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/UpdateMode.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/UpdateMode.java new file mode 100644 index 000000000..3b245d40b --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/UpdateMode.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2018 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; + +import java.util.Arrays; +import java.util.Optional; + +/** + * Enumerates the supported update modes. Each mode represents an attribute + * update strategy. + * + * @see ControllerManagement + */ +public enum UpdateMode { + + /** + * Merge update strategy + */ + MERGE, + + /** + * Replacement update strategy + */ + REPLACE, + + /** + * Removal update strategy + */ + REMOVE; + + public static Optional valueOfIgnoreCase(final String name) { + return Arrays.stream(values()).filter(mode -> mode.name().equalsIgnoreCase(name)).findAny(); + } + +} 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 efa2847f6..1f97097e2 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 @@ -38,6 +38,7 @@ import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.RepositoryConstants; import org.eclipse.hawkbit.repository.RepositoryProperties; import org.eclipse.hawkbit.repository.TenantConfigurationManagement; +import org.eclipse.hawkbit.repository.UpdateMode; import org.eclipse.hawkbit.repository.builder.ActionStatusCreate; import org.eclipse.hawkbit.repository.event.remote.TargetPollEvent; import org.eclipse.hawkbit.repository.event.remote.entity.CancelTargetAssignmentEvent; @@ -666,18 +667,41 @@ public class JpaControllerManagement implements ControllerManagement { @Transactional @Retryable(include = { ConcurrencyFailureException.class }, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) - public Target updateControllerAttributes(final String controllerId, final Map data) { + public Target updateControllerAttributes(final String controllerId, final Map data, + final UpdateMode mode) { final JpaTarget target = (JpaTarget) targetRepository.findByControllerId(controllerId) .orElseThrow(() -> new EntityNotFoundException(Target.class, controllerId)); - target.getControllerAttributes().putAll(data); + // get the modifiable attribute map + final Map controllerAttributes = target.getControllerAttributes(); - if (target.getControllerAttributes().size() > quotaManagement.getMaxAttributeEntriesPerTarget()) { - throw new QuotaExceededException("Controller attribues", target.getControllerAttributes().size(), - quotaManagement.getMaxAttributeEntriesPerTarget()); + final UpdateMode updateMode = mode != null ? mode : UpdateMode.MERGE; + switch (updateMode) { + case REMOVE: + // remove the addressed attributes + data.keySet().forEach(controllerAttributes::remove); + break; + case REPLACE: + // clear the attributes before adding the new attributes + controllerAttributes.clear(); + controllerAttributes.putAll(data); + target.setRequestControllerAttributes(false); + break; + case MERGE: + // just merge the attributes in + controllerAttributes.putAll(data); + target.setRequestControllerAttributes(false); + break; + default: + // unknown update mode + throw new IllegalStateException("The update mode " + updateMode + " is not supported."); } - target.setRequestControllerAttributes(false); + final int attributeCount = controllerAttributes.size(); + if (attributeCount > quotaManagement.getMaxAttributeEntriesPerTarget()) { + throw new QuotaExceededException("Controller attributes", attributeCount, + quotaManagement.getMaxAttributeEntriesPerTarget()); + } return targetRepository.save(target); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java index df7a72808..5c50089fb 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java @@ -16,6 +16,7 @@ import static org.junit.Assert.fail; import java.io.ByteArrayInputStream; import java.net.URISyntaxException; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -24,6 +25,7 @@ import javax.validation.ConstraintViolationException; import org.apache.commons.lang3.RandomUtils; import org.eclipse.hawkbit.repository.RepositoryProperties; +import org.eclipse.hawkbit.repository.UpdateMode; import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; import org.eclipse.hawkbit.repository.event.remote.TargetPollEvent; import org.eclipse.hawkbit.repository.event.remote.entity.ActionCreatedEvent; @@ -121,8 +123,8 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { verifyThrownExceptionBy(() -> controllerManagement.registerRetrieved(NOT_EXIST_IDL, "test message"), "Action"); - verifyThrownExceptionBy(() -> controllerManagement.updateControllerAttributes(NOT_EXIST_ID, Maps.newHashMap()), - "Target"); + verifyThrownExceptionBy( + () -> controllerManagement.updateControllerAttributes(NOT_EXIST_ID, Maps.newHashMap(), null), "Target"); } @Test @@ -658,7 +660,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { private void addAttributeAndVerify(final String controllerId) { final Map testData = Maps.newHashMapWithExpectedSize(1); testData.put("test1", "testdata1"); - controllerManagement.updateControllerAttributes(controllerId, testData); + controllerManagement.updateControllerAttributes(controllerId, testData, null); assertThat(targetManagement.getControllerAttributes(controllerId)).as("Controller Attributes are wrong") .isEqualTo(testData); @@ -668,7 +670,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { private void addSecondAttributeAndVerify(final String controllerId) { final Map testData = Maps.newHashMapWithExpectedSize(2); testData.put("test2", "testdata20"); - controllerManagement.updateControllerAttributes(controllerId, testData); + controllerManagement.updateControllerAttributes(controllerId, testData, null); testData.put("test1", "testdata1"); assertThat(targetManagement.getControllerAttributes(controllerId)).as("Controller Attributes are wrong") @@ -680,13 +682,111 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { final Map testData = Maps.newHashMapWithExpectedSize(2); testData.put("test1", "testdata12"); - controllerManagement.updateControllerAttributes(controllerId, testData); + controllerManagement.updateControllerAttributes(controllerId, testData, null); testData.put("test2", "testdata20"); assertThat(targetManagement.getControllerAttributes(controllerId)).as("Controller Attributes are wrong") .isEqualTo(testData); } + @Test + @Description("Ensures that target attributes can be updated using different update modes.") + @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), + @Expect(type = TargetUpdatedEvent.class, count = 4) }) + public void updateTargetAttributesWithDifferentUpdateModes() { + + final String controllerId = "testCtrl"; + testdataFactory.createTarget(controllerId); + + // no update mode + updateTargetAttributesWithoutUpdateMode(controllerId); + + // update mode REPLACE + updateTargetAttributesWithUpdateModeReplace(controllerId); + + // update mode MERGE + updateTargetAttributesWithUpdateModeMerge(controllerId); + + // update mode REMOVE + updateTargetAttributesWithUpdateModeRemove(controllerId); + + } + + @Step + private void updateTargetAttributesWithUpdateModeRemove(final String controllerId) { + + final int previousSize = targetManagement.getControllerAttributes(controllerId).size(); + + // update the attributes using update mode REMOVE + final Map removeAttributes = new HashMap<>(); + removeAttributes.put("k1", "foo"); + removeAttributes.put("k3", "bar"); + controllerManagement.updateControllerAttributes(controllerId, removeAttributes, UpdateMode.REMOVE); + + // verify attribute removal + final Map updatedAttributes = targetManagement.getControllerAttributes(controllerId); + assertThat(updatedAttributes.size()).isEqualTo(previousSize - 2); + assertThat(updatedAttributes).doesNotContainKeys("k1", "k3"); + + } + + @Step + private void updateTargetAttributesWithUpdateModeMerge(final String controllerId) { + // get the current attributes + final HashMap attributes = new HashMap<>( + targetManagement.getControllerAttributes(controllerId)); + + // update the attributes using update mode MERGE + final Map mergeAttributes = new HashMap<>(); + mergeAttributes.put("k1", "v1_modified_again"); + mergeAttributes.put("k4", "v4"); + controllerManagement.updateControllerAttributes(controllerId, mergeAttributes, UpdateMode.MERGE); + + // verify attribute merge + final Map updatedAttributes = targetManagement.getControllerAttributes(controllerId); + assertThat(updatedAttributes.size()).isEqualTo(4); + assertThat(updatedAttributes).containsAllEntriesOf(mergeAttributes); + assertThat(updatedAttributes.get("k1")).isEqualTo("v1_modified_again"); + attributes.keySet().forEach(assertThat(updatedAttributes)::containsKey); + } + + @Step + private void updateTargetAttributesWithUpdateModeReplace(final String controllerId) { + + // get the current attributes + final HashMap attributes = new HashMap<>( + targetManagement.getControllerAttributes(controllerId)); + + // update the attributes using update mode REPLACE + final Map replacementAttributes = new HashMap<>(); + replacementAttributes.put("k1", "v1_modified"); + replacementAttributes.put("k2", "v2"); + replacementAttributes.put("k3", "v3"); + controllerManagement.updateControllerAttributes(controllerId, replacementAttributes, UpdateMode.REPLACE); + + // verify attribute replacement + final Map updatedAttributes = targetManagement.getControllerAttributes(controllerId); + assertThat(updatedAttributes.size()).isEqualTo(replacementAttributes.size()); + assertThat(updatedAttributes).containsAllEntriesOf(replacementAttributes); + assertThat(updatedAttributes.get("k1")).isEqualTo("v1_modified"); + attributes.entrySet().forEach(assertThat(updatedAttributes)::doesNotContain); + } + + @Step + private void updateTargetAttributesWithoutUpdateMode(final String controllerId) { + + // set the initial attributes + final Map attributes = new HashMap<>(); + attributes.put("k0", "v0"); + attributes.put("k1", "v1"); + controllerManagement.updateControllerAttributes(controllerId, attributes, null); + + // verify initial attributes + final Map updatedAttributes = targetManagement.getControllerAttributes(controllerId); + assertThat(updatedAttributes.size()).isEqualTo(attributes.size()); + assertThat(updatedAttributes).containsAllEntriesOf(attributes); + } + @Test @Description("Ensures that target attribute update fails if quota hits.") @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1), @@ -730,7 +830,7 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { for (int i = 0; i < allowedAttributes; i++) { testData.put(keyPrefix + i, valuePrefix); } - controllerManagement.updateControllerAttributes(controllerId, testData); + controllerManagement.updateControllerAttributes(controllerId, testData, null); } @Test diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementTest.java index 00aea9e47..ac9840ce7 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/TargetManagementTest.java @@ -418,7 +418,7 @@ public class TargetManagementTest extends AbstractJpaIntegrationTest { testData.put("test1", "testdata1"); targetManagement.create(entityFactory.target().create().controllerId(controllerId)); - final Target target = controllerManagement.updateControllerAttributes(controllerId, testData); + final Target target = controllerManagement.updateControllerAttributes(controllerId, testData, null); assertThat(targetManagement.getControllerAttributes(controllerId)).as("Controller Attributes are wrong") .isEqualTo(testData); 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 2c7f4b85d..d473e172a 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 @@ -47,14 +47,14 @@ public class RSQLTargetFieldTest extends AbstractJpaIntegrationTest { target = targetManagement.create(entityFactory.target().create().controllerId("targetId123") .name("targetName123").description("targetDesc123")); attributes.put("revision", "1.1"); - target = controllerManagement.updateControllerAttributes(target.getControllerId(), attributes); + target = controllerManagement.updateControllerAttributes(target.getControllerId(), attributes, null); target = controllerManagement.findOrRegisterTargetIfItDoesNotexist(target.getControllerId(), LOCALHOST); target2 = targetManagement .create(entityFactory.target().create().controllerId("targetId1234").description("targetId1234")); attributes.put("revision", "1.2"); Thread.sleep(1); - target2 = controllerManagement.updateControllerAttributes(target2.getControllerId(), attributes); + target2 = controllerManagement.updateControllerAttributes(target2.getControllerId(), attributes, null); target2 = controllerManagement.findOrRegisterTargetIfItDoesNotexist(target2.getControllerId(), LOCALHOST); final Target target3 = testdataFactory.createTarget("targetId1235"); diff --git a/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java b/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java index 0120f4fda..5c89d1d9a 100644 --- a/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java +++ b/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/util/JsonBuilder.java @@ -387,7 +387,7 @@ public abstract class JsonBuilder { public static String distributionSetUpdateValidFieldsOnly(final DistributionSet set) throws JSONException { - final List modules = set.getModules().stream() + set.getModules().stream() .map(module -> new JSONObject().put("id", module.getId())).collect(Collectors.toList()); return new JSONObject().put("name", set.getName()).put("description", set.getDescription()) @@ -535,13 +535,21 @@ public abstract class JsonBuilder { public static String configData(final String id, final Map attributes, final String execution) throws JSONException { - return new JSONObject().put("id", id).put("time", "20140511T121314") + return configData(id, attributes, execution, null); + } + + public static String configData(final String id, final Map attributes, final String execution, + final String mode) throws JSONException { + final JSONObject json = new JSONObject().put("id", id).put("time", "20140511T121314") .put("status", new JSONObject().put("execution", execution) .put("result", new JSONObject().put("finished", "success")) .put("details", new ArrayList())) - .put("data", attributes).toString(); - + .put("data", attributes); + if (mode != null) { + json.put("mode", mode); + } + return json.toString(); } }