Skip to content

Commit 7b3badd

Browse files
committed
[#1959] Implement JSON Patch for devices, to be *almost* compliant with RFC 6902, allowing to apply the patch to multiples devices.
Signed-off-by: Jean-Baptiste Trystram <[email protected]>
1 parent 2d221f3 commit 7b3badd

File tree

12 files changed

+181
-3
lines changed

12 files changed

+181
-3
lines changed

bom/pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
<flapdoodle.version>2.2.0</flapdoodle.version>
3636
<guava.version>28.0-jre</guava.version>
3737
<caffeine.version>2.8.0</caffeine.version>
38+
<glassfish.version>1.1.4</glassfish.version>
3839
<hamcrest-core.version>2.2</hamcrest-core.version>
3940
<infinispan.version>10.1.8.Final</infinispan.version>
4041
<infinispan.image.name>jboss/infinispan-server:9.4.11.Final</infinispan.image.name>
@@ -107,6 +108,11 @@
107108
<artifactId>hono-client</artifactId>
108109
<version>${project.version}</version>
109110
</dependency>
111+
<dependency>
112+
<groupId>org.glassfish</groupId>
113+
<artifactId>javax.json</artifactId>
114+
<version>${glassfish.version}</version>
115+
</dependency>
110116
<dependency>
111117
<groupId>org.springframework.boot</groupId>
112118
<artifactId>spring-boot</artifactId>

core/src/main/java/org/eclipse/hono/util/RegistryManagementConstants.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public final class RegistryManagementConstants extends RequestResponseApiConstan
5858
*/
5959
public static final String TENANT_HTTP_ENDPOINT = "tenants";
6060

61-
// FIELD DEFINTIONS
61+
// FIELD DEFINITIONS
6262

6363
// DEVICES
6464

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

86+
/**
87+
* The name of the field that contains patch data for a PATCH request.
88+
*/
89+
public static final String FIELD_PATCH_DATA = "patch";
90+
8691
// CREDENTIALS
8792

8893
/**

service-base/src/main/java/org/eclipse/hono/service/http/AbstractHttpEndpoint.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ protected final void extractRequiredJson(final RoutingContext ctx, final Functio
112112
final MIMEHeader contentType = ctx.parsedHeaders().contentType();
113113
if (contentType == null) {
114114
ctx.fail(new ClientErrorException(HttpURLConnection.HTTP_BAD_REQUEST, "Missing Content-Type header"));
115-
} else if (!HttpUtils.CONTENT_TYPE_JSON.equalsIgnoreCase(contentType.value())) {
115+
} else if ( !(HttpUtils.CONTENT_TYPE_JSON.equalsIgnoreCase(contentType.value()) ||
116+
HttpUtils.CONTENT_TYPE_JSON_PATCH.equalsIgnoreCase(contentType.value()))) {
116117
ctx.fail(new ClientErrorException(HttpURLConnection.HTTP_BAD_REQUEST, "Unsupported Content-Type"));
117118
} else {
118119
try {

service-base/src/main/java/org/eclipse/hono/service/http/HttpUtils.java

+4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ public final class HttpUtils {
4444
* The <em>application/json</em> content type.
4545
*/
4646
public static final String CONTENT_TYPE_JSON = "application/json";
47+
/**
48+
* The <em>application/json-patch</em> content type.
49+
*/
50+
public static final String CONTENT_TYPE_JSON_PATCH = "application/json-patch";
4751
/**
4852
* The <em>application/json; charset=utf-8</em> content type.
4953
*/

services/device-registry-base/pom.xml

+4
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
<groupId>org.hamcrest</groupId>
3232
<artifactId>hamcrest-core</artifactId>
3333
</dependency>
34+
<dependency>
35+
<groupId>org.glassfish</groupId>
36+
<artifactId>javax.json</artifactId>
37+
</dependency>
3438
</dependencies>
3539

3640
<build>

services/device-registry-base/src/main/java/org/eclipse/hono/deviceregistry/service/device/AbstractDeviceManagementService.java

+29
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
*******************************************************************************/
1313
package org.eclipse.hono.deviceregistry.service.device;
1414

15+
import java.util.List;
1516
import java.util.Objects;
1617
import java.util.Optional;
1718
import java.util.UUID;
@@ -27,6 +28,7 @@
2728

2829
import io.opentracing.Span;
2930
import io.vertx.core.Future;
31+
import io.vertx.core.json.JsonArray;
3032

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

95+
/**
96+
* Apply a patch to a list of devices.
97+
*
98+
* @param tenantId The tenant the device belongs to.
99+
* @param deviceIds A list of devices ID to apply the patch to.
100+
* @param patchData The actual data to patch.
101+
* @param span The active OpenTracing span for this operation.
102+
* @return A future indicating the outcome of the operation.
103+
*/
104+
protected abstract Future<Result<Void>> processPatchDevice(String tenantId, List deviceIds,
105+
JsonArray patchData, Span span);
106+
93107
/**
94108
* Generates a unique device identifier for a given tenant. A default implementation generates a random UUID value.
95109
*
@@ -157,4 +171,19 @@ public Future<Result<Void>> deleteDevice(final String tenantId, final String dev
157171
: processDeleteDevice(DeviceKey.from(result.getPayload(), deviceId), resourceVersion, span));
158172

159173
}
174+
175+
@Override
176+
public Future<Result<Void>> patchDevice(final String tenantId, final List deviceIds, final JsonArray patch, final Span span) {
177+
178+
Objects.requireNonNull(tenantId);
179+
Objects.requireNonNull(deviceIds);
180+
Objects.requireNonNull(patch);
181+
182+
return this.tenantInformationService
183+
.tenantExists(tenantId, span)
184+
.compose(result -> result.isError()
185+
? Future.succeededFuture(Result.from(result.getStatus()))
186+
: processPatchDevice(tenantId, deviceIds, patch, span));
187+
188+
}
160189
}

services/device-registry-base/src/main/java/org/eclipse/hono/service/management/device/DelegatingDeviceManagementHttpEndpoint.java

+84-1
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,22 @@
1313

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

16+
import java.io.StringReader;
1617
import java.net.HttpURLConnection;
1718
import java.util.EnumSet;
19+
import java.util.List;
1820
import java.util.Objects;
1921
import java.util.Optional;
2022

23+
import javax.json.Json;
24+
import javax.json.JsonPatch;
25+
2126
import org.eclipse.hono.client.ClientErrorException;
2227
import org.eclipse.hono.config.ServiceConfigProperties;
2328
import org.eclipse.hono.service.http.TracingHandler;
2429
import org.eclipse.hono.service.management.AbstractDelegatingRegistryHttpEndpoint;
2530
import org.eclipse.hono.service.management.Id;
31+
import org.eclipse.hono.service.management.OperationResult;
2632
import org.eclipse.hono.tracing.TracingHelper;
2733
import org.eclipse.hono.util.RegistryManagementConstants;
2834

@@ -34,6 +40,7 @@
3440
import io.vertx.core.http.HttpHeaders;
3541
import io.vertx.core.http.HttpMethod;
3642
import io.vertx.core.json.DecodeException;
43+
import io.vertx.core.json.JsonArray;
3744
import io.vertx.core.json.JsonObject;
3845
import io.vertx.ext.web.Router;
3946
import io.vertx.ext.web.RoutingContext;
@@ -54,6 +61,7 @@ public class DelegatingDeviceManagementHttpEndpoint<S extends DeviceManagementSe
5461
private static final String SPAN_NAME_GET_DEVICE = "get Device from management API";
5562
private static final String SPAN_NAME_UPDATE_DEVICE = "update Device from management API";
5663
private static final String SPAN_NAME_REMOVE_DEVICE = "remove Device from management API";
64+
private static final String SPAN_NAME_PATCH_DEVICES = "patch Devices from management API";
5765

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

8593
// Add CORS handler
86-
router.route(pathWithTenant).handler(createCorsHandler(config.getCorsAllowedOrigin(), EnumSet.of(HttpMethod.POST)));
94+
router.route(pathWithTenant).handler(createCorsHandler(config.getCorsAllowedOrigin(), EnumSet.of(HttpMethod.POST, HttpMethod.PATCH)));
8795
router.route(pathWithTenantAndDeviceId).handler(createDefaultCorsHandler(config.getCorsAllowedOrigin()));
8896

8997

@@ -111,6 +119,11 @@ public void addRoutes(final Router router) {
111119
router.delete(pathWithTenantAndDeviceId)
112120
.handler(this::extractIfMatchVersionParam)
113121
.handler(this::doDeleteDevice);
122+
123+
// PATCH devices
124+
router.patch(pathWithTenant)
125+
.handler(this::extractRequiredJsonPayload)
126+
.handler(this::doPatchDevices);
114127
}
115128

116129
private void doGetDevice(final RoutingContext ctx) {
@@ -212,6 +225,76 @@ private void doDeleteDevice(final RoutingContext ctx) {
212225
.onComplete(s -> span.finish());
213226
}
214227

228+
private void doPatchDevices(final RoutingContext ctx) {
229+
230+
final Span span = TracingHelper.buildServerChildSpan(
231+
tracer,
232+
TracingHandler.serverSpanContext(ctx),
233+
SPAN_NAME_PATCH_DEVICES,
234+
getClass().getSimpleName()
235+
).start();
236+
237+
final Future<String> tenantId = getRequestParameter(ctx, PARAM_TENANT_ID, getPredicate(config.getTenantIdPattern(), false));
238+
239+
// NOTE that the remaining code would be executed in any case, i.e.
240+
// even if any of the parameters retrieved from the RoutingContext were null
241+
// However, this will not happen because of the way the routes are set up,
242+
// i.e. a request for a URI that doesn't contain a device ID will result
243+
// in a 404 response.
244+
245+
final JsonObject payload = ctx.get(KEY_REQUEST_BODY);
246+
final List<String> deviceList = payload.getJsonArray(RegistryManagementConstants.FIELD_ID).getList();
247+
final JsonArray patch = payload.getJsonArray(RegistryManagementConstants.FIELD_PATCH_DATA);
248+
249+
// try to call the implementing service
250+
getService().patchDevice(tenantId.result(), deviceList, patch, span).onComplete(handler -> {
251+
if (handler.result().getStatus() == HttpURLConnection.HTTP_NOT_IMPLEMENTED) {
252+
253+
// if not implemented we can do it here.
254+
final JsonObject response = new JsonObject();
255+
final javax.json.JsonArray formatedPatch = Json.createReader(new StringReader(patch.encode())).readArray();
256+
final JsonPatch jsonpatch = Json.createPatch(formatedPatch);
257+
for (String devId : deviceList) {
258+
259+
getService().readDevice(tenantId.result(), devId, span)
260+
.onComplete(r -> {
261+
262+
if (r.result().getStatus() == HttpURLConnection.HTTP_OK) {
263+
final JsonObject jsonDevice = JsonObject.mapFrom(r.result().getPayload());
264+
javax.json.JsonObject device = Json.createReader(new StringReader(jsonDevice.encode())).readObject();
265+
266+
// apply the patch to the existing device
267+
device = jsonpatch.apply(device);
268+
269+
//update the registry with the new payload
270+
final Device updatedDevice = new JsonObject(device.toString()).mapTo(Device.class);
271+
getService().updateDevice(tenantId.result(), devId, updatedDevice, Optional.empty(), span)
272+
.onComplete(u -> {
273+
response.put(devId, new JsonObject()
274+
.put("status", u.result().getStatus())
275+
.put("resource-version", u.result().getResourceVersion().orElse("")));
276+
});
277+
278+
} else {
279+
response.put(devId, new JsonObject()
280+
.put("status", r.result().getStatus())
281+
// the registry doesn't issue an error message.
282+
.put("error-message", String.format("device '%s' cannot be retrieved", devId))
283+
);
284+
}
285+
});
286+
287+
}
288+
final OperationResult result = OperationResult.ok(HttpURLConnection.HTTP_CREATED, response, Optional.empty(), Optional.empty());
289+
writeResponse(ctx, result, null, span);
290+
291+
// the service implemented the feature.
292+
} else {
293+
writeResponse(ctx, handler.result(), null, span);
294+
}
295+
});
296+
}
297+
215298
/**
216299
* Gets the device from the request body.
217300
*

services/device-registry-base/src/main/java/org/eclipse/hono/service/management/device/DeviceManagementService.java

+22
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

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

16+
import java.util.List;
1617
import java.util.Optional;
1718

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

2223
import io.opentracing.Span;
2324
import io.vertx.core.Future;
25+
import io.vertx.core.json.JsonArray;
2426

2527
/**
2628
* A service for managing device registration information.
@@ -106,4 +108,24 @@ Future<OperationResult<Id>> updateDevice(String tenantId, String deviceId, Devic
106108
* Device Registry Management API - Delete Device Registration</a>
107109
*/
108110
Future<Result<Void>> deleteDevice(String tenantId, String deviceId, Optional<String> resourceVersion, Span span);
111+
112+
/**
113+
* Apply a patch to a list of devices.
114+
*
115+
* @param tenantId The tenant the device belongs to.
116+
* @param deviceIds A list of devices ID to apply the patch to.
117+
* @param patch The Json patch, following RFC 6902.
118+
* @param span The active OpenTracing span for this operation. It is not to be closed in this method!
119+
* An implementation should log (error) events on this span and it may set tags and use this span as the
120+
* parent for any spans created in this method.
121+
* @return A future indicating the outcome of the operation.
122+
* The <em>status code</em> is set as specified in the
123+
* <a href="https://www.eclipse.org/hono/docs/api/management/#/devices/patchRegistration">
124+
* Device Registry Management API - Patch Device Registration </a>
125+
* @throws NullPointerException if any of the parameters is {@code null}.
126+
* @see <a href="https://www.eclipse.org/hono/docs/api/management/#/devices/patchRegistration">
127+
* Device Registry Management API - Patch Device Registration</a>
128+
*/
129+
Future<Result<Void>> patchDevice(String tenantId, List deviceIds, JsonArray patch, Span span);
130+
109131
}

services/device-registry-file/src/main/java/org/eclipse/hono/deviceregistry/file/FileBasedDeviceBackend.java

+7
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import io.vertx.core.CompositeFuture;
4848
import io.vertx.core.Future;
4949
import io.vertx.core.Promise;
50+
import io.vertx.core.json.JsonArray;
5051
import io.vertx.core.json.JsonObject;
5152

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

175+
@Override
176+
public Future<Result<Void>> patchDevice(final String tenantId, final List deviceIds,
177+
final JsonArray patch, final Span span) {
178+
return registrationService.patchDevice(tenantId, deviceIds, patch, span);
179+
}
180+
174181
// CREDENTIALS
175182

176183
@Override

services/device-registry-file/src/main/java/org/eclipse/hono/deviceregistry/file/FileBasedRegistrationService.java

+5
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,11 @@ public Future<OperationResult<Id>> createDevice(
430430
return Future.succeededFuture(processCreateDevice(tenantId, deviceId, device, span));
431431
}
432432

433+
@Override
434+
public Future<Result<Void>> patchDevice(final String tenantId, final List deviceIds, final JsonArray patch, final Span span) {
435+
return Future.succeededFuture(OperationResult.from(HttpURLConnection.HTTP_NOT_IMPLEMENTED));
436+
}
437+
433438
/**
434439
* Adds a device to this registry.
435440
*

services/device-registry-mongodb/src/main/java/org/eclipse/hono/deviceregistry/mongodb/service/MongoDbBasedDeviceBackend.java

+7
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import io.opentracing.Span;
3636
import io.opentracing.noop.NoopSpan;
3737
import io.vertx.core.Future;
38+
import io.vertx.core.json.JsonArray;
3839
import io.vertx.core.json.JsonObject;
3940

4041
/**
@@ -96,6 +97,12 @@ public Future<Result<Void>> deleteDevice(final String tenantId, final String dev
9697
});
9798
}
9899

100+
@Override
101+
public Future<Result<Void>> patchDevice(final String tenantId, final List deviceIds,
102+
final JsonArray patch, final Span span) {
103+
return registrationService.patchDevice(tenantId, deviceIds, patch, span);
104+
}
105+
99106
@Override
100107
public Future<OperationResult<Id>> createDevice(
101108
final String tenantId,

services/device-registry-mongodb/src/main/java/org/eclipse/hono/deviceregistry/mongodb/service/MongoDbBasedRegistrationService.java

+5
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,11 @@ public Future<Result<Void>> deleteDevice(final String tenantId, final String dev
177177
.recover(error -> Future.succeededFuture(MongoDbDeviceRegistryUtils.mapErrorToResult(error, span)));
178178
}
179179

180+
@Override
181+
public Future<Result<Void>> patchDevice(final String tenantId, final List deviceIds, final JsonArray patch, final Span span) {
182+
return Future.succeededFuture(OperationResult.from(HttpURLConnection.HTTP_NOT_IMPLEMENTED));
183+
}
184+
180185
/**
181186
* {@inheritDoc}
182187
*/

0 commit comments

Comments
 (0)