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

Fix JSON content format implementation #762

Merged
merged 2 commits into from
Jan 2, 2020
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -80,64 +80,62 @@ public static List<TimestampedLwM2mNode> decodeTimestamped(byte[] content, LwM2m
}
}

private static List<TimestampedLwM2mNode> parseJSON(JsonRootObject jsonObject, LwM2mPath path, LwM2mModel model,
Class<? extends LwM2mNode> nodeClass) throws CodecException {
private static List<TimestampedLwM2mNode> parseJSON(JsonRootObject jsonObject, LwM2mPath requestPath,
LwM2mModel model, Class<? extends LwM2mNode> nodeClass) throws CodecException {

LOG.trace("Parsing JSON content for path {}: {}", path, jsonObject);
LOG.trace("Parsing JSON content for path {}: {}", requestPath, jsonObject);

// Group JSON entry by time-stamp
Map<Long, Collection<JsonArrayEntry>> jsonEntryByTimestamp = groupJsonEntryByTimestamp(jsonObject);

// Extract baseName
LwM2mPath baseName = extractAndValidateBaseName(jsonObject, path);
if (baseName == null)
baseName = path; // if no base name, use request path as base name
String baseName = jsonObject.getBaseName() == null ? "" : jsonObject.getBaseName();

// fill time-stamped nodes collection
List<TimestampedLwM2mNode> timestampedNodes = new ArrayList<>();
for (Entry<Long, Collection<JsonArrayEntry>> entryByTimestamp : jsonEntryByTimestamp.entrySet()) {

// Group JSON entry by instance
Map<Integer, Collection<JsonArrayEntry>> jsonEntryByInstanceId = groupJsonEntryByInstanceId(
entryByTimestamp.getValue(), baseName);
entryByTimestamp.getValue(), baseName, requestPath);

// Create lwm2m node
LwM2mNode node;
if (nodeClass == LwM2mObject.class) {
Collection<LwM2mObjectInstance> instances = new ArrayList<>();
for (Entry<Integer, Collection<JsonArrayEntry>> entryByInstanceId : jsonEntryByInstanceId.entrySet()) {
Map<Integer, LwM2mResource> resourcesMap = extractLwM2mResources(entryByInstanceId.getValue(),
baseName, model);
baseName, model, requestPath);

instances.add(new LwM2mObjectInstance(entryByInstanceId.getKey(), resourcesMap.values()));
}

node = new LwM2mObject(baseName.getObjectId(), instances);
node = new LwM2mObject(requestPath.getObjectId(), instances);
} else if (nodeClass == LwM2mObjectInstance.class) {
// validate we have resources for only 1 instance
if (jsonEntryByInstanceId.size() != 1)
throw new CodecException("One instance expected in the payload [path:%s]", path);
throw new CodecException("One instance expected in the payload [path:%s]", requestPath);

// Extract resources
Entry<Integer, Collection<JsonArrayEntry>> instanceEntry = jsonEntryByInstanceId.entrySet().iterator()
.next();
Map<Integer, LwM2mResource> resourcesMap = extractLwM2mResources(instanceEntry.getValue(), baseName,
model);
model, requestPath);

// Create instance
node = new LwM2mObjectInstance(instanceEntry.getKey(), resourcesMap.values());
} else if (nodeClass == LwM2mResource.class) {
// validate we have resources for only 1 instance
if (jsonEntryByInstanceId.size() > 1)
throw new CodecException("Only one instance expected in the payload [path:%s]", path);
throw new CodecException("Only one instance expected in the payload [path:%s]", requestPath);

// Extract resources
Map<Integer, LwM2mResource> resourcesMap = extractLwM2mResources(
jsonEntryByInstanceId.values().iterator().next(), baseName, model);
jsonEntryByInstanceId.values().iterator().next(), baseName, model, requestPath);

// validate there is only 1 resource
if (resourcesMap.size() != 1)
throw new CodecException("One resource should be present in the payload [path:%s]", path);
throw new CodecException("One resource should be present in the payload [path:%s]", requestPath);

node = resourcesMap.values().iterator().next();
} else {
Expand Down Expand Up @@ -221,12 +219,12 @@ public int compare(Long o1, Long o2) {
* @return a map (instanceId => collection of JsonArrayEntry)
*/
private static Map<Integer, Collection<JsonArrayEntry>> groupJsonEntryByInstanceId(
Collection<JsonArrayEntry> jsonEntries, LwM2mPath baseName) throws CodecException {
Collection<JsonArrayEntry> jsonEntries, String baseName, LwM2mPath requestPath) throws CodecException {
Map<Integer, Collection<JsonArrayEntry>> result = new HashMap<>();

for (JsonArrayEntry e : jsonEntries) {
// Build resource path
LwM2mPath nodePath = baseName.append(e.getName());
LwM2mPath nodePath = extractAndValidatePath(baseName, e.getName() == null ? "" : e.getName(), requestPath);

// Validate path
if (!nodePath.isResourceInstance() && !nodePath.isResource()) {
Expand All @@ -247,45 +245,49 @@ private static Map<Integer, Collection<JsonArrayEntry>> groupJsonEntryByInstance
}

// Create an entry for an empty instance if possible
if (result.isEmpty() && baseName.getObjectInstanceId() != null) {
result.put(baseName.getObjectInstanceId(), new ArrayList<JsonArrayEntry>());
if (result.isEmpty()) {
if (baseName.isEmpty()) {
// search object instance id in request path
if (requestPath.getObjectInstanceId() != null) {
result.put(requestPath.getObjectInstanceId(), new ArrayList<JsonArrayEntry>());
}
} else {
// search object instance id in basename
LwM2mPath basePath = extractAndValidatePath(baseName, "", requestPath);
if (basePath.getObjectInstanceId() != null)
result.put(basePath.getObjectInstanceId(), new ArrayList<JsonArrayEntry>());
}
}
return result;
}

private static LwM2mPath extractAndValidateBaseName(JsonRootObject jsonObject, LwM2mPath requestPath)
private static LwM2mPath extractAndValidatePath(String baseName, String name, LwM2mPath requestPath)
throws CodecException {
// Check baseName is valid
if (jsonObject.getBaseName() != null && !jsonObject.getBaseName().isEmpty()) {
LwM2mPath bnPath = new LwM2mPath(jsonObject.getBaseName());

// check returned base name path is under requested path
if (requestPath.getObjectId() != null && bnPath.getObjectId() != null) {
if (!bnPath.getObjectId().equals(requestPath.getObjectId())) {
throw new CodecException("Basename path [%s] does not match requested path [%s].", bnPath,
LwM2mPath path = new LwM2mPath(baseName + name);

// check returned path is under requested path
if (requestPath.getObjectId() != null && path.getObjectId() != null) {
if (!path.getObjectId().equals(requestPath.getObjectId())) {
throw new CodecException("resource path [%s] does not match requested path [%s].", path, requestPath);
}
if (requestPath.getObjectInstanceId() != null && path.getObjectInstanceId() != null) {
if (!path.getObjectInstanceId().equals(requestPath.getObjectInstanceId())) {
throw new CodecException("Basename path [%s] does not match requested path [%s].", path,
requestPath);
}
if (requestPath.getObjectInstanceId() != null && bnPath.getObjectInstanceId() != null) {
if (!bnPath.getObjectInstanceId().equals(requestPath.getObjectInstanceId())) {
throw new CodecException("Basename path [%s] does not match requested path [%s].", bnPath,
if (requestPath.getResourceId() != null && path.getResourceId() != null) {
if (!path.getResourceId().equals(requestPath.getResourceId())) {
throw new CodecException("Basename path [%s] does not match requested path [%s].", path,
requestPath);
}
if (requestPath.getResourceId() != null && bnPath.getResourceId() != null) {
if (!bnPath.getResourceId().equals(requestPath.getResourceId())) {
throw new CodecException("Basename path [%s] does not match requested path [%s].", bnPath,
requestPath);
}
}
}
}
return bnPath;
}
return null;

return path;
}

private static Map<Integer, LwM2mResource> extractLwM2mResources(Collection<JsonArrayEntry> jsonArrayEntries,
LwM2mPath baseName, LwM2mModel model) throws CodecException {
String baseName, LwM2mModel model, LwM2mPath requestPath) throws CodecException {
if (jsonArrayEntries == null)
return Collections.emptyMap();

Expand All @@ -294,8 +296,13 @@ private static Map<Integer, LwM2mResource> extractLwM2mResources(Collection<Json
Map<LwM2mPath, Map<Integer, JsonArrayEntry>> multiResourceMap = new HashMap<>();
for (JsonArrayEntry resourceElt : jsonArrayEntries) {

// Build resource path
LwM2mPath nodePath = baseName.append(resourceElt.getName());
// Build resource path (path validation was already done in groupJsonEntryByInstanceId
LwM2mPath nodePath;
if (resourceElt.getName() == null) {
nodePath = new LwM2mPath(baseName);
} else {
nodePath = new LwM2mPath(baseName + resourceElt.getName());
}

// handle LWM2M resources
if (nodePath.isResourceInstance()) {
Expand Down Expand Up @@ -356,13 +363,21 @@ private static Map<Integer, LwM2mResource> extractLwM2mResources(Collection<Json
}

// If we found nothing, we try to create an empty multi-instance resource
if (lwM2mResourceMap.isEmpty() && baseName.isResource()) {
ResourceModel resourceModel = model.getResourceModel(baseName.getObjectId(), baseName.getResourceId());
// We create it only if this respect the model
if (resourceModel == null || resourceModel.multiple) {
Type resourceType = getResourceType(baseName, model, null);
lwM2mResourceMap.put(baseName.getResourceId(), LwM2mMultipleResource
.newResource(baseName.getResourceId(), new HashMap<Integer, Object>(), resourceType));
if (lwM2mResourceMap.isEmpty()) {
LwM2mPath path;
if (baseName.isEmpty()) {
path = requestPath;
} else {
path = extractAndValidatePath(baseName, "", requestPath);
}
if (path.getObjectId() != null && path.getResourceId() != null) {
ResourceModel resourceModel = model.getResourceModel(path.getObjectId(), path.getResourceId());
// We create it only if this respect the model
if (resourceModel == null || resourceModel.multiple) {
Type resourceType = getResourceType(path, model, null);
lwM2mResourceMap.put(path.getResourceId(), LwM2mMultipleResource.newResource(path.getResourceId(),
new HashMap<Integer, Object>(), resourceType));
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public static byte[] encode(LwM2mNode node, LwM2mPath path, LwM2mModel model, Lw
node.accept(internalEncoder);
JsonRootObject jsonObject = new JsonRootObject();
jsonObject.setResourceList(internalEncoder.resourceList);
jsonObject.setBaseName(path.toString());
jsonObject.setBaseName(internalEncoder.baseName);
return LwM2mJson.toJsonLwM2m(jsonObject).getBytes();
}

Expand All @@ -70,6 +70,7 @@ public static byte[] encodeTimestampedData(List<TimestampedLwM2mNode> timestampe

InternalEncoder internalEncoder = new InternalEncoder();
ArrayList<JsonArrayEntry> entries = new ArrayList<>();
String baseName = null;
for (TimestampedLwM2mNode timestampedLwM2mNode : timestampedNodes) {
internalEncoder.objectId = path.getObjectId();
internalEncoder.model = model;
Expand All @@ -79,10 +80,19 @@ public static byte[] encodeTimestampedData(List<TimestampedLwM2mNode> timestampe
internalEncoder.timestamp = timestampedLwM2mNode.getTimestamp();
timestampedLwM2mNode.getNode().accept(internalEncoder);
entries.addAll(internalEncoder.resourceList);
if (baseName != null) {
if (!baseName.equals(internalEncoder.baseName)) {
throw new CodecException(
"Unexpected baseName %s (%s expected) when encoding timestamped nodes for request %s",
internalEncoder.baseName, baseName, path);
}
} else {
baseName = internalEncoder.baseName;
}
}
JsonRootObject jsonObject = new JsonRootObject();
jsonObject.setResourceList(entries);
jsonObject.setBaseName(path.toString());
jsonObject.setBaseName(internalEncoder.baseName);
return LwM2mJson.toJsonLwM2m(jsonObject).getBytes();
}

Expand All @@ -96,6 +106,7 @@ private static class InternalEncoder implements LwM2mNodeVisitor {

// visitor output
private ArrayList<JsonArrayEntry> resourceList = null;
private String baseName = null;

@Override
public void visit(LwM2mObject object) {
Expand All @@ -104,6 +115,7 @@ public void visit(LwM2mObject object) {
if (!requestPath.isObject()) {
throw new CodecException("Invalid request path %s for JSON object encoding", requestPath);
}
baseName = requestPath.toString() + "/";

// Create resources
resourceList = new ArrayList<>();
Expand All @@ -119,6 +131,9 @@ public void visit(LwM2mObject object) {
public void visit(LwM2mObjectInstance instance) {
LOG.trace("Encoding object instance {} into JSON", instance);
resourceList = new ArrayList<>();
if (instance.getId() == LwM2mObjectInstance.UNDEFINED) {
throw new CodecException("Unable to use JSON format without to give the object instance Id");
}
for (LwM2mResource resource : instance.getResources().values()) {
// Validate request path & compute resource path
String prefixPath;
Expand All @@ -129,6 +144,7 @@ public void visit(LwM2mObjectInstance instance) {
} else {
throw new CodecException("Invalid request path %s for JSON instance encoding", requestPath);
}
baseName = requestPath + "/";
// Create resources
resourceList.addAll(lwM2mResourceToJsonArrayEntry(prefixPath, timestamp, resource));
}
Expand All @@ -140,8 +156,12 @@ public void visit(LwM2mResource resource) {
if (!requestPath.isResource()) {
throw new CodecException("Invalid request path %s for JSON resource encoding", requestPath);
}

resourceList = lwM2mResourceToJsonArrayEntry("", timestamp, resource);
if (resource.isMultiInstances()) {
baseName = requestPath.toString() + "/";
} else {
baseName = requestPath.toString();
}
resourceList = lwM2mResourceToJsonArrayEntry(null, timestamp, resource);
}

private ArrayList<JsonArrayEntry> lwM2mResourceToJsonArrayEntry(String resourcePath, Long timestamp,
Expand Down Expand Up @@ -183,7 +203,7 @@ private ArrayList<JsonArrayEntry> lwM2mResourceToJsonArrayEntry(String resourceP
jsonResourceElt.setTime(timestamp);

// Convert value using expected type
LwM2mPath lwM2mResourcePath = new LwM2mPath(resourcePath);
LwM2mPath lwM2mResourcePath = resourcePath != null ? new LwM2mPath(resourcePath) : null;
this.setResourceValue(converter.convertValue(resource.getValue(), resource.getType(), expectedType,
lwM2mResourcePath), expectedType, jsonResourceElt, lwM2mResourcePath);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,11 @@ private CreateRequest(ContentFormat format, LwM2mPath target, Integer instanceId
this.instanceId = instanceId;
this.resources = Collections.unmodifiableList(Arrays.asList(resources));
this.contentFormat = format != null ? format : ContentFormat.TLV; // default to TLV

if (this.contentFormat == ContentFormat.JSON && instanceId == null) {
throw new InvalidRequestException(
"Missing object instance id for CREATE request (%s) using JSON content format.", target);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ public class JsonArrayEntrySerDes extends JsonSerDes<JsonArrayEntry> {
@Override
public JsonObject jSerialize(JsonArrayEntry jae) {
JsonObject o = new JsonObject();
o.add("n", jae.getName());
if (jae.getName() != null)
o.add("n", jae.getName());
Type type = jae.getType();
if (type != null) {
switch (jae.getType()) {
Expand All @@ -43,7 +44,7 @@ public JsonObject jSerialize(JsonArrayEntry jae) {
o.add("sv", jae.getStringValue());
break;
default:
break;
throw new LwM2mJsonException("JsonArrayEntry MUST have a value : %s", jae);
}
}
if (jae.getTime() != null)
Expand Down Expand Up @@ -79,6 +80,10 @@ public JsonArrayEntry deserialize(JsonObject o) {
if (ov != null && ov.isString())
jae.setObjectLinkValue(ov.asString());

if (jae.getType() == null) {
throw new LwM2mJsonException("Missing value(v,bv,ov,sv) field for entry %s", o.toString());
}

return jae;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ public JsonRootObject deserialize(JsonObject o) {
JsonValue e = o.get("e");
if (e != null)
jro.setResourceList(serDes.deserialize(e.asArray()));
else
throw new LwM2mJsonException("'e' field is missing for %s", o.toString());

JsonValue bn = o.get("bn");
if (bn != null && bn.isString())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,22 @@
/**
* Exception thrown in case of JSON parsing error
*/
public class LwM2mJsonException extends Exception {
public class LwM2mJsonException extends RuntimeException {

private static final long serialVersionUID = 1L;

public LwM2mJsonException(String message) {
super(message);
}

public LwM2mJsonException(String message, Object... args) {
super(String.format(message, args));
}

public LwM2mJsonException(Exception e, String message, Object... args) {
super(String.format(message, args), e);
}

public LwM2mJsonException(String message, Exception cause) {
super(message, cause);
}
Expand Down
Loading