diff --git a/hawkbit-rest/hawkbit-ddi-api/pom.xml b/hawkbit-rest/hawkbit-ddi-api/pom.xml
index fafba9afa..932048a9d 100644
--- a/hawkbit-rest/hawkbit-ddi-api/pom.xml
+++ b/hawkbit-rest/hawkbit-ddi-api/pom.xml
@@ -29,6 +29,10 @@
com.fasterxml.jackson.core
jackson-annotations
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-cbor
+
javax.validation
validation-api
diff --git a/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRestConstants.java b/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRestConstants.java
index bacb70ee4..cf04baa45 100644
--- a/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRestConstants.java
+++ b/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRestConstants.java
@@ -50,6 +50,19 @@ public final class DdiRestConstants {
*/
public static final String NO_ACTION_HISTORY = "0";
+ /**
+ * Media type for CBOR content. Unfortunately, there is no other constant we
+ * can reuse - even the Jackson data converter simply hardcodes this.
+ */
+ public static final String MEDIA_TYPE_CBOR = "application/cbor";
+
+ /**
+ * Media type for CBOR content with strings encoded as UTF-8. Technically
+ * redundant since CBOR always uses UTF-8, but Spring will append it
+ * regardless.
+ */
+ public static final String MEDIA_TYPE_CBOR_UTF8 = "application/cbor;charset=UTF-8";
+
private DdiRestConstants() {
// constant class, private constructor.
}
diff --git a/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRootControllerRestApi.java b/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRootControllerRestApi.java
index 7b759c773..619ed1849 100644
--- a/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRootControllerRestApi.java
+++ b/hawkbit-rest/hawkbit-ddi-api/src/main/java/org/eclipse/hawkbit/ddi/rest/api/DdiRootControllerRestApi.java
@@ -50,7 +50,7 @@ public interface DdiRootControllerRestApi {
* @return the response
*/
@GetMapping(value = "/{controllerId}/softwaremodules/{softwareModuleId}/artifacts", produces = {
- MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE })
+ MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
ResponseEntity> getSoftwareModulesArtifacts(@PathVariable("tenant") final String tenant,
@PathVariable("controllerId") final String controllerId,
@PathVariable("softwareModuleId") final Long softwareModuleId);
@@ -66,7 +66,8 @@ public interface DdiRootControllerRestApi {
* the HTTP request injected by spring
* @return the response
*/
- @GetMapping(value = "/{controllerId}", produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE })
+ @GetMapping(value = "/{controllerId}", produces = { MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE,
+ DdiRestConstants.MEDIA_TYPE_CBOR })
ResponseEntity getControllerBase(@PathVariable("tenant") final String tenant,
@PathVariable("controllerId") final String controllerId);
@@ -154,7 +155,7 @@ public interface DdiRootControllerRestApi {
* @return the response
*/
@GetMapping(value = "/{controllerId}/" + DdiRestConstants.DEPLOYMENT_BASE_ACTION + "/{actionId}", produces = {
- MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE })
+ MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
ResponseEntity getControllerBasedeploymentAction(@PathVariable("tenant") final String tenant,
@PathVariable("controllerId") @NotEmpty final String controllerId,
@PathVariable("actionId") @NotEmpty final Long actionId,
@@ -178,7 +179,8 @@ public interface DdiRootControllerRestApi {
* @return the response
*/
@PostMapping(value = "/{controllerId}/" + DdiRestConstants.DEPLOYMENT_BASE_ACTION + "/{actionId}/"
- + DdiRestConstants.FEEDBACK, consumes = MediaType.APPLICATION_JSON_VALUE)
+ + DdiRestConstants.FEEDBACK, consumes = { MediaType.APPLICATION_JSON_VALUE,
+ DdiRestConstants.MEDIA_TYPE_CBOR })
ResponseEntity postBasedeploymentActionFeedback(@Valid final DdiActionFeedback feedback,
@PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId,
@PathVariable("actionId") @NotEmpty final Long actionId);
@@ -197,8 +199,8 @@ public interface DdiRootControllerRestApi {
*
* @return status of the request
*/
- @PutMapping(value = "/{controllerId}/"
- + DdiRestConstants.CONFIG_DATA_ACTION, consumes = MediaType.APPLICATION_JSON_VALUE)
+ @PutMapping(value = "/{controllerId}/" + DdiRestConstants.CONFIG_DATA_ACTION, consumes = {
+ MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
ResponseEntity putConfigData(@Valid final DdiConfigData configData,
@PathVariable("tenant") final String tenant, @PathVariable("controllerId") final String controllerId);
@@ -217,7 +219,7 @@ public interface DdiRootControllerRestApi {
* @return the {@link DdiCancel} response
*/
@GetMapping(value = "/{controllerId}/" + DdiRestConstants.CANCEL_ACTION + "/{actionId}", produces = {
- MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE })
+ MediaTypes.HAL_JSON_VALUE, MediaType.APPLICATION_JSON_VALUE, DdiRestConstants.MEDIA_TYPE_CBOR })
ResponseEntity getControllerCancelAction(@PathVariable("tenant") final String tenant,
@PathVariable("controllerId") @NotEmpty final String controllerId,
@PathVariable("actionId") @NotEmpty final Long actionId);
@@ -241,7 +243,8 @@ public interface DdiRootControllerRestApi {
*/
@PostMapping(value = "/{controllerId}/" + DdiRestConstants.CANCEL_ACTION + "/{actionId}/"
- + DdiRestConstants.FEEDBACK, consumes = MediaType.APPLICATION_JSON_VALUE)
+ + DdiRestConstants.FEEDBACK, consumes = { MediaType.APPLICATION_JSON_VALUE,
+ DdiRestConstants.MEDIA_TYPE_CBOR })
ResponseEntity postCancelActionFeedback(@Valid final DdiActionFeedback feedback,
@PathVariable("tenant") final String tenant,
@PathVariable("controllerId") @NotEmpty final String controllerId,
diff --git a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/AbstractDDiApiIntegrationTest.java b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/AbstractDDiApiIntegrationTest.java
index b79f55a02..d54a0ed40 100644
--- a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/AbstractDDiApiIntegrationTest.java
+++ b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/AbstractDDiApiIntegrationTest.java
@@ -8,6 +8,11 @@
*/
package org.eclipse.hawkbit.ddi.rest.resource;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+
import org.eclipse.hawkbit.repository.jpa.RepositoryApplicationConfiguration;
import org.eclipse.hawkbit.repository.test.TestConfiguration;
import org.eclipse.hawkbit.rest.AbstractRestIntegrationTest;
@@ -16,9 +21,59 @@ import org.springframework.cloud.stream.test.binder.TestSupportBinderAutoConfigu
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
+import com.fasterxml.jackson.dataformat.cbor.CBORGenerator;
+import com.fasterxml.jackson.dataformat.cbor.CBORParser;
+
@ContextConfiguration(classes = { DdiApiConfiguration.class, RestConfiguration.class,
RepositoryApplicationConfiguration.class, TestConfiguration.class, TestSupportBinderAutoConfiguration.class })
@TestPropertySource(locations = "classpath:/ddi-test.properties")
public abstract class AbstractDDiApiIntegrationTest extends AbstractRestIntegrationTest {
+ /**
+ * Convert JSON to a CBOR equivalent.
+ *
+ * @param json
+ * JSON object to convert
+ * @return Equivalent CBOR data
+ * @throws IOException
+ * Invalid JSON input
+ */
+ protected static byte[] jsonToCbor(String json) throws IOException {
+ JsonFactory jsonFactory = new JsonFactory();
+ JsonParser jsonParser = jsonFactory.createParser(json);
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ CBORFactory cborFactory = new CBORFactory();
+ CBORGenerator cborGenerator = cborFactory.createGenerator(out);
+ while (jsonParser.nextToken() != null) {
+ cborGenerator.copyCurrentEvent(jsonParser);
+ }
+ cborGenerator.flush();
+ return out.toByteArray();
+ }
+
+ /**
+ * Convert CBOR to JSON equivalent.
+ *
+ * @param input
+ * CBOR data to convert
+ * @return Equivalent JSON string
+ * @throws IOException
+ * Invalid CBOR input
+ */
+ protected static String cborToJson(byte[] input) throws IOException {
+ CBORFactory cborFactory = new CBORFactory();
+ CBORParser cborParser = cborFactory.createParser(input);
+ JsonFactory jsonFactory = new JsonFactory();
+ StringWriter stringWriter = new StringWriter();
+ JsonGenerator jsonGenerator = jsonFactory.createGenerator(stringWriter);
+ while (cborParser.nextToken() != null) {
+ jsonGenerator.copyCurrentEvent(cborParser);
+ }
+ jsonGenerator.flush();
+ return stringWriter.toString();
+ }
}
diff --git a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiCancelActionTest.java b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiCancelActionTest.java
index bb2817f91..495abe560 100644
--- a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiCancelActionTest.java
+++ b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiCancelActionTest.java
@@ -23,6 +23,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import java.util.ArrayList;
import java.util.List;
+import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.Action.Status;
import org.eclipse.hawkbit.repository.model.DistributionSet;
@@ -33,6 +34,8 @@ import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter;
import org.junit.Test;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.MediaType;
+import org.springframework.integration.json.JsonPathUtils;
+import org.springframework.test.util.JsonPathExpectationsHelper;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
@@ -45,6 +48,32 @@ import io.qameta.allure.Story;
@Story("Cancel Action Resource")
public class DdiCancelActionTest extends AbstractDDiApiIntegrationTest {
+ @Test
+ @Description("Tests that the cancel action resource can be used with CBOR.")
+ public void cancelActionCbor() throws Exception {
+ final DistributionSet ds = testdataFactory.createDistributionSet("");
+ final Target savedTarget = testdataFactory.createTarget();
+ final Long actionId = assignDistributionSet(ds.getId(), TestdataFactory.DEFAULT_CONTROLLER_ID).getActions()
+ .get(0);
+ final Action cancelAction = deploymentManagement.cancelAction(actionId);
+
+ // check that we can get the cancel action as CBOR
+ byte[] result = mvc.perform(get("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
+ + cancelAction.getId(), tenantAware.getCurrentTenant()).accept(DdiRestConstants.MEDIA_TYPE_CBOR))
+ .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
+ .andExpect(content().contentType(DdiRestConstants.MEDIA_TYPE_CBOR_UTF8))
+ .andReturn().getResponse().getContentAsByteArray();
+ assertThat(JsonPathUtils.evaluate(cborToJson(result), "$.id")).isEqualTo(String.valueOf(cancelAction.getId()));
+ assertThat(JsonPathUtils.evaluate(cborToJson(result), "$.cancelAction.stopId")).isEqualTo(String.valueOf(actionId));
+
+ // and submit feedback as CBOR
+ mvc.perform(post("/{tenant}/controller/v1/" + TestdataFactory.DEFAULT_CONTROLLER_ID + "/cancelAction/"
+ + cancelAction.getId() + "/feedback", tenantAware.getCurrentTenant()).content(
+ jsonToCbor(JsonBuilder.cancelActionFeedback(cancelAction.getId().toString(), "proceeding")))
+ .contentType(DdiRestConstants.MEDIA_TYPE_CBOR).accept(DdiRestConstants.MEDIA_TYPE_CBOR))
+ .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
+ }
+
@Test
@Description("Test of the controller can continue a started update even after a cancel command if it so desires.")
public void rootRsCancelActionButContinueAnyway() throws Exception {
diff --git a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiConfigDataTest.java b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiConfigDataTest.java
index b0c60f6c3..8e675ce65 100644
--- a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiConfigDataTest.java
+++ b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiConfigDataTest.java
@@ -22,6 +22,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
+import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
import org.eclipse.hawkbit.exception.SpServerError;
import org.eclipse.hawkbit.repository.exception.InvalidTargetAttributeException;
import org.eclipse.hawkbit.repository.exception.QuotaExceededException;
@@ -54,6 +55,21 @@ public class DdiConfigDataTest extends AbstractDDiApiIntegrationTest {
Target.CONTROLLER_ATTRIBUTE_VALUE_SIZE + 1);
private static final String VALUE_VALID = generateRandomStringWithLength(Target.CONTROLLER_ATTRIBUTE_VALUE_SIZE);
+ @Test
+ @Description("Verify that config data can be uploaded as CBOR")
+ public void putConfigDataAsCbor() throws Exception {
+ testdataFactory.createTarget("4717");
+
+ final Map attributes = new HashMap<>();
+ attributes.put(KEY_VALID, VALUE_VALID);
+
+ mvc.perform(put("/{tenant}/controller/v1/4717/configData", tenantAware.getCurrentTenant())
+ .content(jsonToCbor(JsonBuilder.configData("", attributes, "closed").toString()))
+ .contentType(DdiRestConstants.MEDIA_TYPE_CBOR)).andDo(MockMvcResultPrinter.print())
+ .andExpect(status().isOk());
+ assertThat(targetManagement.getControllerAttributes("4717")).isEqualTo(attributes);
+ }
+
@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.")
diff --git a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiDeploymentBaseTest.java b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiDeploymentBaseTest.java
index 8e60a2cf8..5a342edc2 100644
--- a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiDeploymentBaseTest.java
+++ b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiDeploymentBaseTest.java
@@ -29,6 +29,7 @@ import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.RandomUtils;
import org.assertj.core.api.Condition;
+import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.ActionCreatedEvent;
import org.eclipse.hawkbit.repository.event.remote.entity.DistributionSetCreatedEvent;
@@ -71,6 +72,40 @@ public class DdiDeploymentBaseTest extends AbstractDDiApiIntegrationTest {
private static final String HTTP_LOCALHOST = "http://localhost:8080/";
+ @Test
+ @Description("Ensure that the deployment resource is available as CBOR")
+ public void deploymentResourceCbor() throws Exception {
+ final Target target = testdataFactory.createTarget();
+ final DistributionSet distributionSet = testdataFactory.createDistributionSet("");
+
+ assignDistributionSet(distributionSet.getId(), target.getName());
+ final Action uaction = deploymentManagement.findActiveActionsByTarget(PAGE, target.getControllerId())
+ .getContent().get(0);
+
+ // get deployment base
+ mvc.perform(get("/{tenant}/controller/v1/{target}/deploymentBase/" + uaction.getId(),
+ tenantAware.getCurrentTenant(), target.getControllerId()).accept(DdiRestConstants.MEDIA_TYPE_CBOR))
+ .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
+ .andExpect(content().contentType(DdiRestConstants.MEDIA_TYPE_CBOR_UTF8));
+
+ final Long softwareModuleId = distributionSet.getModules().stream().findAny().get().getId();
+ testdataFactory.createArtifacts(softwareModuleId);
+ // get artifacts
+ mvc.perform(get("/{tenant}/controller/v1/{target}/softwaremodules/{softwareModuleId}/artifacts",
+ tenantAware.getCurrentTenant(), target.getControllerId(), softwareModuleId)
+ .accept(DdiRestConstants.MEDIA_TYPE_CBOR))
+ .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
+ .andExpect(content().contentType(DdiRestConstants.MEDIA_TYPE_CBOR_UTF8));
+
+ // submit feedback
+ final byte[] feedback = jsonToCbor(
+ JsonBuilder.deploymentActionFeedback(uaction.getId().toString(), "proceeding"));
+ mvc.perform(post("/{tenant}/controller/v1/{target}/deploymentBase/" + uaction.getId() + "/feedback",
+ tenantAware.getCurrentTenant(), target.getControllerId()).content(feedback)
+ .contentType(DdiRestConstants.MEDIA_TYPE_CBOR).accept(DdiRestConstants.MEDIA_TYPE_CBOR))
+ .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk());
+ }
+
@Test
@Description("Ensures that artifacts are not found, when softare module does not exists.")
public void artifactsNotFound() throws Exception {
diff --git a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java
index e31fd0d6b..2a431802a 100644
--- a/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java
+++ b/hawkbit-rest/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java
@@ -30,6 +30,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import java.util.Collections;
import java.util.Map;
+import org.eclipse.hawkbit.ddi.rest.api.DdiRestConstants;
import org.eclipse.hawkbit.im.authentication.SpPermission;
import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent;
import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent;
@@ -59,6 +60,7 @@ import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import io.qameta.allure.Description;
@@ -79,6 +81,26 @@ public class DdiRootControllerTest extends AbstractDDiApiIntegrationTest {
@Autowired
private HawkbitSecurityProperties securityProperties;
+ @Test
+ @Description("Ensure that the root poll resource is available as CBOR")
+ public void rootPollResourceCbor() throws Exception {
+ mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant())
+ .accept(DdiRestConstants.MEDIA_TYPE_CBOR)).andDo(MockMvcResultPrinter.print())
+ .andExpect(content().contentType(DdiRestConstants.MEDIA_TYPE_CBOR_UTF8)).andExpect(status().isOk());
+ }
+
+ @Test
+ @Description("Ensures that the API returns JSON when no Accept header is specified by the client.")
+ public void apiReturnsJSONByDefault() throws Exception {
+ final MvcResult result = mvc.perform(get("/{tenant}/controller/v1/4711", tenantAware.getCurrentTenant()))
+ .andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
+ .andExpect(content().contentType(MediaTypes.HAL_JSON_UTF8)).andReturn();
+
+ // verify that we did not specify a content-type in the request, in case
+ // there are any default values
+ assertThat(result.getRequest().getHeader("Accept")).isNull();
+ }
+
@Test
@Description("Ensures that targets cannot be created e.g. in plug'n play scenarios when tenant does not exists but can be created if the tenant exists.")
@WithUser(tenantId = "tenantDoesNotExists", allSpPermissions = true, authorities = { CONTROLLER_ROLE,