Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement JSON Patch for multiples devices #2039

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<flapdoodle.version>2.2.0</flapdoodle.version>
<guava.version>28.0-jre</guava.version>
<caffeine.version>2.8.0</caffeine.version>
<glassfish.version>1.1.4</glassfish.version>
<hamcrest-core.version>2.2</hamcrest-core.version>
<infinispan.version>10.1.8.Final</infinispan.version>
<infinispan.image.name>jboss/infinispan-server:9.4.11.Final</infinispan.image.name>
Expand Down Expand Up @@ -107,6 +108,11 @@
<artifactId>hono-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
<version>${glassfish.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public final class RegistryManagementConstants extends RequestResponseApiConstan
*/
public static final String TENANT_HTTP_ENDPOINT = "tenants";

// FIELD DEFINTIONS
// FIELD DEFINITIONS

// DEVICES

Expand All @@ -83,6 +83,11 @@ public final class RegistryManagementConstants extends RequestResponseApiConstan
*/
public static final String FIELD_MEMBER_OF = "memberOf";

/**
* The name of the field that contains patch data for a PATCH request.
*/
public static final String FIELD_PATCH_DATA = "patch";

// CREDENTIALS

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@ protected final void extractRequiredJson(final RoutingContext ctx, final Functio
final MIMEHeader contentType = ctx.parsedHeaders().contentType();
if (contentType == null) {
ctx.fail(new ClientErrorException(HttpURLConnection.HTTP_BAD_REQUEST, "Missing Content-Type header"));
} else if (!HttpUtils.CONTENT_TYPE_JSON.equalsIgnoreCase(contentType.value())) {
} else if ( !(HttpUtils.CONTENT_TYPE_JSON.equalsIgnoreCase(contentType.value()) ||
HttpUtils.CONTENT_TYPE_JSON_PATCH.equalsIgnoreCase(contentType.value()))) {
ctx.fail(new ClientErrorException(HttpURLConnection.HTTP_BAD_REQUEST, "Unsupported Content-Type"));
} else {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ public final class HttpUtils {
* The <em>application/json</em> content type.
*/
public static final String CONTENT_TYPE_JSON = "application/json";
/**
* The <em>application/json-patch</em> content type.
*/
public static final String CONTENT_TYPE_JSON_PATCH = "application/json-patch";
/**
* The <em>application/json; charset=utf-8</em> content type.
*/
Expand Down
4 changes: 4 additions & 0 deletions services/device-registry-base/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*******************************************************************************/
package org.eclipse.hono.deviceregistry.service.device;

import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
Expand All @@ -27,6 +28,7 @@

import io.opentracing.Span;
import io.vertx.core.Future;
import io.vertx.core.json.JsonArray;

/**
* An abstract base class implementation for {@link DeviceManagementService}.
Expand Down Expand Up @@ -90,6 +92,18 @@ public void setTenantInformationService(final TenantInformationService tenantInf
*/
protected abstract Future<Result<Void>> processDeleteDevice(DeviceKey key, Optional<String> resourceVersion, Span span);

/**
* Apply a patch to a list of devices.
*
* @param tenantId The tenant the device belongs to.
* @param deviceIds A list of devices ID to apply the patch to.
* @param patchData The actual data to patch.
* @param span The active OpenTracing span for this operation.
* @return A future indicating the outcome of the operation.
*/
protected abstract Future<Result<Void>> processPatchDevice(String tenantId, List deviceIds,
JsonArray patchData, Span span);

/**
* Generates a unique device identifier for a given tenant. A default implementation generates a random UUID value.
*
Expand Down Expand Up @@ -157,4 +171,19 @@ public Future<Result<Void>> deleteDevice(final String tenantId, final String dev
: processDeleteDevice(DeviceKey.from(result.getPayload(), deviceId), resourceVersion, span));

}

@Override
public Future<Result<Void>> patchDevice(final String tenantId, final List deviceIds, final JsonArray patch, final Span span) {

Objects.requireNonNull(tenantId);
Objects.requireNonNull(deviceIds);
Objects.requireNonNull(patch);

return this.tenantInformationService
.tenantExists(tenantId, span)
.compose(result -> result.isError()
? Future.succeededFuture(Result.from(result.getStatus()))
: processPatchDevice(tenantId, deviceIds, patch, span));

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,22 @@

package org.eclipse.hono.service.management.device;

import java.io.StringReader;
import java.net.HttpURLConnection;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import javax.json.Json;
import javax.json.JsonPatch;

import org.eclipse.hono.client.ClientErrorException;
import org.eclipse.hono.config.ServiceConfigProperties;
import org.eclipse.hono.service.http.TracingHandler;
import org.eclipse.hono.service.management.AbstractDelegatingRegistryHttpEndpoint;
import org.eclipse.hono.service.management.Id;
import org.eclipse.hono.service.management.OperationResult;
import org.eclipse.hono.tracing.TracingHelper;
import org.eclipse.hono.util.RegistryManagementConstants;

Expand All @@ -34,6 +40,7 @@
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.json.DecodeException;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
Expand All @@ -54,6 +61,7 @@ public class DelegatingDeviceManagementHttpEndpoint<S extends DeviceManagementSe
private static final String SPAN_NAME_GET_DEVICE = "get Device from management API";
private static final String SPAN_NAME_UPDATE_DEVICE = "update Device from management API";
private static final String SPAN_NAME_REMOVE_DEVICE = "remove Device from management API";
private static final String SPAN_NAME_PATCH_DEVICES = "patch Devices from management API";

private static final String DEVICE_MANAGEMENT_ENDPOINT_NAME = String.format("%s/%s",
RegistryManagementConstants.API_VERSION,
Expand Down Expand Up @@ -83,7 +91,7 @@ public void addRoutes(final Router router) {
PARAM_DEVICE_ID);

// Add CORS handler
router.route(pathWithTenant).handler(createCorsHandler(config.getCorsAllowedOrigin(), EnumSet.of(HttpMethod.POST)));
router.route(pathWithTenant).handler(createCorsHandler(config.getCorsAllowedOrigin(), EnumSet.of(HttpMethod.POST, HttpMethod.PATCH)));
router.route(pathWithTenantAndDeviceId).handler(createDefaultCorsHandler(config.getCorsAllowedOrigin()));


Expand Down Expand Up @@ -111,6 +119,11 @@ public void addRoutes(final Router router) {
router.delete(pathWithTenantAndDeviceId)
.handler(this::extractIfMatchVersionParam)
.handler(this::doDeleteDevice);

// PATCH devices
router.patch(pathWithTenant)
.handler(this::extractRequiredJsonPayload)
.handler(this::doPatchDevices);
}

private void doGetDevice(final RoutingContext ctx) {
Expand Down Expand Up @@ -212,6 +225,76 @@ private void doDeleteDevice(final RoutingContext ctx) {
.onComplete(s -> span.finish());
}

private void doPatchDevices(final RoutingContext ctx) {

final Span span = TracingHelper.buildServerChildSpan(
tracer,
TracingHandler.serverSpanContext(ctx),
SPAN_NAME_PATCH_DEVICES,
getClass().getSimpleName()
).start();

final Future<String> tenantId = getRequestParameter(ctx, PARAM_TENANT_ID, getPredicate(config.getTenantIdPattern(), false));

// NOTE that the remaining code would be executed in any case, i.e.
// even if any of the parameters retrieved from the RoutingContext were null
// However, this will not happen because of the way the routes are set up,
// i.e. a request for a URI that doesn't contain a device ID will result
// in a 404 response.

final JsonObject payload = ctx.get(KEY_REQUEST_BODY);
final List<String> deviceList = payload.getJsonArray(RegistryManagementConstants.FIELD_ID).getList();
final JsonArray patch = payload.getJsonArray(RegistryManagementConstants.FIELD_PATCH_DATA);

// try to call the implementing service
getService().patchDevice(tenantId.result(), deviceList, patch, span).onComplete(handler -> {
if (handler.result().getStatus() == HttpURLConnection.HTTP_NOT_IMPLEMENTED) {

// if not implemented we can do it here.
final JsonObject response = new JsonObject();
final javax.json.JsonArray formatedPatch = Json.createReader(new StringReader(patch.encode())).readArray();
final JsonPatch jsonpatch = Json.createPatch(formatedPatch);
for (String devId : deviceList) {

getService().readDevice(tenantId.result(), devId, span)
.onComplete(r -> {

if (r.result().getStatus() == HttpURLConnection.HTTP_OK) {
final JsonObject jsonDevice = JsonObject.mapFrom(r.result().getPayload());
javax.json.JsonObject device = Json.createReader(new StringReader(jsonDevice.encode())).readObject();

// apply the patch to the existing device
device = jsonpatch.apply(device);

//update the registry with the new payload
final Device updatedDevice = new JsonObject(device.toString()).mapTo(Device.class);
getService().updateDevice(tenantId.result(), devId, updatedDevice, Optional.empty(), span)
.onComplete(u -> {
response.put(devId, new JsonObject()
.put("status", u.result().getStatus())
.put("resource-version", u.result().getResourceVersion().orElse("")));
});

} else {
response.put(devId, new JsonObject()
.put("status", r.result().getStatus())
// the registry doesn't issue an error message.
.put("error-message", String.format("device '%s' cannot be retrieved", devId))
);
}
});

}
final OperationResult result = OperationResult.ok(HttpURLConnection.HTTP_CREATED, response, Optional.empty(), Optional.empty());
writeResponse(ctx, result, null, span);

// the service implemented the feature.
} else {
writeResponse(ctx, handler.result(), null, span);
}
});
}

/**
* Gets the device from the request body.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

package org.eclipse.hono.service.management.device;

import java.util.List;
import java.util.Optional;

import org.eclipse.hono.service.management.Id;
Expand All @@ -21,6 +22,7 @@

import io.opentracing.Span;
import io.vertx.core.Future;
import io.vertx.core.json.JsonArray;

/**
* A service for managing device registration information.
Expand Down Expand Up @@ -106,4 +108,24 @@ Future<OperationResult<Id>> updateDevice(String tenantId, String deviceId, Devic
* Device Registry Management API - Delete Device Registration</a>
*/
Future<Result<Void>> deleteDevice(String tenantId, String deviceId, Optional<String> resourceVersion, Span span);

/**
* Apply a patch to a list of devices.
*
* @param tenantId The tenant the device belongs to.
* @param deviceIds A list of devices ID to apply the patch to.
* @param patch The Json patch, following RFC 6902.
* @param span The active OpenTracing span for this operation. It is not to be closed in this method!
* An implementation should log (error) events on this span and it may set tags and use this span as the
* parent for any spans created in this method.
* @return A future indicating the outcome of the operation.
* The <em>status code</em> is set as specified in the
* <a href="https://www.eclipse.org/hono/docs/api/management/#/devices/patchRegistration">
* Device Registry Management API - Patch Device Registration </a>
* @throws NullPointerException if any of the parameters is {@code null}.
* @see <a href="https://www.eclipse.org/hono/docs/api/management/#/devices/patchRegistration">
* Device Registry Management API - Patch Device Registration</a>
*/
Future<Result<Void>> patchDevice(String tenantId, List deviceIds, JsonArray patch, Span span);

}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import io.vertx.core.CompositeFuture;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;

/**
Expand Down Expand Up @@ -171,6 +172,12 @@ public Future<OperationResult<Id>> updateDevice(final String tenantId, final Str
return registrationService.updateDevice(tenantId, deviceId, device, resourceVersion, span);
}

@Override
public Future<Result<Void>> patchDevice(final String tenantId, final List deviceIds,
final JsonArray patch, final Span span) {
return registrationService.patchDevice(tenantId, deviceIds, patch, span);
}

// CREDENTIALS

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,11 @@ public Future<OperationResult<Id>> createDevice(
return Future.succeededFuture(processCreateDevice(tenantId, deviceId, device, span));
}

@Override
public Future<Result<Void>> patchDevice(final String tenantId, final List deviceIds, final JsonArray patch, final Span span) {
return Future.succeededFuture(OperationResult.from(HttpURLConnection.HTTP_NOT_IMPLEMENTED));
}

/**
* Adds a device to this registry.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import io.opentracing.Span;
import io.opentracing.noop.NoopSpan;
import io.vertx.core.Future;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;

/**
Expand Down Expand Up @@ -96,6 +97,12 @@ public Future<Result<Void>> deleteDevice(final String tenantId, final String dev
});
}

@Override
public Future<Result<Void>> patchDevice(final String tenantId, final List deviceIds,
final JsonArray patch, final Span span) {
return registrationService.patchDevice(tenantId, deviceIds, patch, span);
}

@Override
public Future<OperationResult<Id>> createDevice(
final String tenantId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,11 @@ public Future<Result<Void>> deleteDevice(final String tenantId, final String dev
.recover(error -> Future.succeededFuture(MongoDbDeviceRegistryUtils.mapErrorToResult(error, span)));
}

@Override
public Future<Result<Void>> patchDevice(final String tenantId, final List deviceIds, final JsonArray patch, final Span span) {
return Future.succeededFuture(OperationResult.from(HttpURLConnection.HTTP_NOT_IMPLEMENTED));
}

/**
* {@inheritDoc}
*/
Expand Down