/******************************************************************************* * Copyright (c) 2015 Sierra Wireless and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * and Eclipse Distribution License v1.0 which accompany this distribution. * * The Eclipse Public License is available at * http://www.eclipse.org/legal/epl-v10.html * and the Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.html. * * Contributors: * Sierra Wireless - initial API and implementation *******************************************************************************/ package org.eclipse.leshan.core.node.codec.json; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.SortedMap; import java.util.TreeMap; import org.eclipse.leshan.core.model.LwM2mModel; import org.eclipse.leshan.core.model.ResourceModel; import org.eclipse.leshan.core.model.ResourceModel.Type; import org.eclipse.leshan.core.node.LwM2mMultipleResource; import org.eclipse.leshan.core.node.LwM2mNode; import org.eclipse.leshan.core.node.LwM2mObject; import org.eclipse.leshan.core.node.LwM2mObjectInstance; import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.node.LwM2mSingleResource; import org.eclipse.leshan.core.node.TimestampedLwM2mNode; import org.eclipse.leshan.core.node.codec.CodecException; import org.eclipse.leshan.json.JsonArrayEntry; import org.eclipse.leshan.json.JsonRootObject; import org.eclipse.leshan.json.LwM2mJson; import org.eclipse.leshan.json.LwM2mJsonException; import org.eclipse.leshan.util.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class LwM2mNodeJsonDecoder { private static final Logger LOG = LoggerFactory.getLogger(LwM2mNodeJsonDecoder.class); @SuppressWarnings("unchecked") public static <T extends LwM2mNode> T decode(byte[] content, LwM2mPath path, LwM2mModel model, Class<T> nodeClass) throws CodecException { try { String jsonStrValue = content != null ? new String(content) : ""; JsonRootObject json = LwM2mJson.fromJsonLwM2m(jsonStrValue); List<TimestampedLwM2mNode> timestampedNodes = parseJSON(json, path, model, nodeClass); if (timestampedNodes.size() == 0) { return null; } else { // return the most recent value return (T) timestampedNodes.get(0).getNode(); } } catch (LwM2mJsonException e) { throw new CodecException(e, "Unable to deserialize json [path:%s]", path); } } public static List<TimestampedLwM2mNode> decodeTimestamped(byte[] content, LwM2mPath path, LwM2mModel model, Class<? extends LwM2mNode> nodeClass) throws CodecException { try { String jsonStrValue = new String(content); JsonRootObject json = LwM2mJson.fromJsonLwM2m(jsonStrValue); return parseJSON(json, path, model, nodeClass); } catch (LwM2mJsonException e) { throw new CodecException(e, "Unable to deserialize json [path:%s]", path); } } private static List<TimestampedLwM2mNode> parseJSON(JsonRootObject jsonObject, LwM2mPath path, LwM2mModel model, Class<? extends LwM2mNode> nodeClass) throws CodecException { LOG.trace("Parsing JSON content for path {}: {}", path, 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 // 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); // 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); instances.add(new LwM2mObjectInstance(entryByInstanceId.getKey(), resourcesMap.values())); } node = new LwM2mObject(baseName.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); // Extract resources Entry<Integer, Collection<JsonArrayEntry>> instanceEntry = jsonEntryByInstanceId.entrySet().iterator() .next(); Map<Integer, LwM2mResource> resourcesMap = extractLwM2mResources(instanceEntry.getValue(), baseName, model); // 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); // Extract resources Map<Integer, LwM2mResource> resourcesMap = extractLwM2mResources( jsonEntryByInstanceId.values().iterator().next(), baseName, model); // validate there is only 1 resource if (resourcesMap.size() != 1) throw new CodecException("One resource should be present in the payload [path:%s]", path); node = resourcesMap.values().iterator().next(); } else { throw new IllegalArgumentException("invalid node class: " + nodeClass); } // compute time-stamp Long timestamp = computeTimestamp(jsonObject.getBaseTime(), entryByTimestamp.getKey()); // add time-stamped node timestampedNodes.add(new TimestampedLwM2mNode(timestamp, node)); } return timestampedNodes; } private static Long computeTimestamp(Long baseTime, Long time) { Long timestamp; if (baseTime != null) { if (time != null) { timestamp = baseTime + time; } else { timestamp = baseTime; } } else { if (time != null) { timestamp = time; } else { timestamp = null; } } return timestamp; } /** * Group all JsonArrayEntry by time-stamp * * @return a map (relativeTimestamp => collection of JsonArrayEntry) */ private static SortedMap<Long, Collection<JsonArrayEntry>> groupJsonEntryByTimestamp(JsonRootObject jsonObject) { SortedMap<Long, Collection<JsonArrayEntry>> result = new TreeMap<>(new Comparator<Long>() { @Override public int compare(Long o1, Long o2) { // comparator which // - supports null (time null means 0 if there is a base time) // - reverses natural order (most recent value in first) return Long.compare(o2 == null ? 0 : o2, o1 == null ? 0 : o1); } }); for (JsonArrayEntry e : jsonObject.getResourceList()) { // Get time for this entry Long time = e.getTime(); // Get jsonArray for this time-stamp Collection<JsonArrayEntry> jsonArray = result.get(time); if (jsonArray == null) { jsonArray = new ArrayList<>(); result.put(time, jsonArray); } // Add it to the list jsonArray.add(e); } // Ensure there is at least one entry for null timestamp if (result.isEmpty()) { result.put((Long) null, new ArrayList<JsonArrayEntry>()); } return result; } /** * Group all JsonArrayEntry by instanceId * * @param jsonEntries * @param baseName * * @return a map (instanceId => collection of JsonArrayEntry) */ private static Map<Integer, Collection<JsonArrayEntry>> groupJsonEntryByInstanceId( Collection<JsonArrayEntry> jsonEntries, LwM2mPath baseName) throws CodecException { Map<Integer, Collection<JsonArrayEntry>> result = new HashMap<>(); for (JsonArrayEntry e : jsonEntries) { // Build resource path LwM2mPath nodePath = baseName.append(e.getName()); // Validate path if (!nodePath.isResourceInstance() && !nodePath.isResource()) { throw new CodecException( "Invalid path [%s] for resource, it should be a resource or a resource instance path", nodePath); } // Get jsonArray for this instance Collection<JsonArrayEntry> jsonArray = result.get(nodePath.getObjectInstanceId()); if (jsonArray == null) { jsonArray = new ArrayList<>(); result.put(nodePath.getObjectInstanceId(), jsonArray); } // Add it to the list jsonArray.add(e); } // Create an entry for an empty instance if possible if (result.isEmpty() && baseName.getObjectInstanceId() != null) { result.put(baseName.getObjectInstanceId(), new ArrayList<JsonArrayEntry>()); } return result; } private static LwM2mPath extractAndValidateBaseName(JsonRootObject jsonObject, 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, 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, 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; } private static Map<Integer, LwM2mResource> extractLwM2mResources(Collection<JsonArrayEntry> jsonArrayEntries, LwM2mPath baseName, LwM2mModel model) throws CodecException { if (jsonArrayEntries == null) return Collections.emptyMap(); // Extract LWM2M resources from JSON resource list Map<Integer, LwM2mResource> lwM2mResourceMap = new HashMap<>(); Map<LwM2mPath, Map<Integer, JsonArrayEntry>> multiResourceMap = new HashMap<>(); for (JsonArrayEntry resourceElt : jsonArrayEntries) { // Build resource path LwM2mPath nodePath = baseName.append(resourceElt.getName()); // handle LWM2M resources if (nodePath.isResourceInstance()) { // Multi-instance resource // Store multi-instance resource values in a map // we will deal with it later LwM2mPath resourcePath = new LwM2mPath(nodePath.getObjectId(), nodePath.getObjectInstanceId(), nodePath.getResourceId()); Map<Integer, JsonArrayEntry> multiResource = multiResourceMap.get(resourcePath); if (multiResource == null) { multiResource = new HashMap<>(); multiResourceMap.put(resourcePath, multiResource); } multiResource.put(nodePath.getResourceInstanceId(), resourceElt); } else if (nodePath.isResource()) { // Single resource Type expectedType = getResourceType(nodePath, model, resourceElt); LwM2mResource res = LwM2mSingleResource.newResource(nodePath.getResourceId(), parseJsonValue(resourceElt.getResourceValue(), expectedType, nodePath), expectedType); lwM2mResourceMap.put(nodePath.getResourceId(), res); } else { throw new CodecException( "Invalid path [%s] for resource, it should be a resource or a resource instance path", nodePath); } } // Handle multi-instance resource. for (Map.Entry<LwM2mPath, Map<Integer, JsonArrayEntry>> entry : multiResourceMap.entrySet()) { LwM2mPath resourcePath = entry.getKey(); Map<Integer, JsonArrayEntry> jsonEntries = entry.getValue(); if (jsonEntries != null && !jsonEntries.isEmpty()) { Type expectedType = getResourceType(resourcePath, model, jsonEntries.values().iterator().next()); Map<Integer, Object> values = new HashMap<>(); for (Entry<Integer, JsonArrayEntry> e : jsonEntries.entrySet()) { Integer resourceInstanceId = e.getKey(); values.put(resourceInstanceId, parseJsonValue(e.getValue().getResourceValue(), expectedType, resourcePath)); } LwM2mResource resource = LwM2mMultipleResource.newResource(resourcePath.getResourceId(), values, expectedType); lwM2mResourceMap.put(resourcePath.getResourceId(), resource); } } // 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)); } } return lwM2mResourceMap; } private static Object parseJsonValue(Object value, Type expectedType, LwM2mPath path) throws CodecException { LOG.trace("JSON value for path {} and expected type {}: {}", path, expectedType, value); try { switch (expectedType) { case INTEGER: // JSON format specs said v = integer or float return ((Number) value).longValue(); case BOOLEAN: return value; case FLOAT: // JSON format specs said v = integer or float return ((Number) value).doubleValue(); case TIME: return new Date(((Number) value).longValue() * 1000L); case OPAQUE: // If the Resource data type is opaque the string value // holds the Base64 encoded representation of the Resource return Base64.decodeBase64((String) value); case STRING: return value; default: throw new CodecException("Unsupported type %s for path %s", expectedType, path); } } catch (Exception e) { throw new CodecException(e, "Invalid content [%s] for type %s for path %s", value, expectedType, path); } } public static Type getResourceType(LwM2mPath rscPath, LwM2mModel model, JsonArrayEntry resourceElt) { // Use model type in priority ResourceModel rscDesc = model.getResourceModel(rscPath.getObjectId(), rscPath.getResourceId()); if (rscDesc != null && rscDesc.type != null) return rscDesc.type; // Then json type if (resourceElt != null) { Type type = resourceElt.getType(); if (type != null) return type; } // Else use String as default LOG.trace("unknown type for resource use string as default: {}", rscPath); return Type.STRING; } }