/******************************************************************************* * 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.tlv; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.eclipse.leshan.core.model.LwM2mModel; import org.eclipse.leshan.core.model.ObjectModel; 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.codec.CodecException; import org.eclipse.leshan.tlv.Tlv; import org.eclipse.leshan.tlv.Tlv.TlvType; import org.eclipse.leshan.tlv.TlvDecoder; import org.eclipse.leshan.tlv.TlvException; import org.eclipse.leshan.util.Hex; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class LwM2mNodeTlvDecoder { private static final Logger LOG = LoggerFactory.getLogger(LwM2mNodeTlvDecoder.class); public static <T extends LwM2mNode> T decode(byte[] content, LwM2mPath path, LwM2mModel model, Class<T> nodeClass) throws CodecException { try { Tlv[] tlvs = TlvDecoder.decode(ByteBuffer.wrap(content != null ? content : new byte[0])); return parseTlv(tlvs, path, model, nodeClass); } catch (TlvException e) { throw new CodecException(String.format("Unable to decode tlv for path [%s]", path), e); } } @SuppressWarnings("unchecked") private static <T extends LwM2mNode> T parseTlv(Tlv[] tlvs, LwM2mPath path, LwM2mModel model, Class<T> nodeClass) throws CodecException { LOG.trace("Parsing TLV content for path {}: {}", path, tlvs); // Object if (nodeClass == LwM2mObject.class) { List<LwM2mObjectInstance> instances = new ArrayList<>(); // is it an array of TLV resources? if (tlvs.length > 0 && // (tlvs[0].getType() == TlvType.MULTIPLE_RESOURCE || tlvs[0].getType() == TlvType.RESOURCE_VALUE)) { ObjectModel oModel = model.getObjectModel(path.getObjectId()); if (oModel == null) { LOG.warn("No model for object {}. The tlv is decoded assuming this is a single instance object", path.getObjectId()); instances.add(parseObjectInstanceTlv(tlvs, path.getObjectId(), 0, model)); } else if (!oModel.multiple) { instances.add(parseObjectInstanceTlv(tlvs, path.getObjectId(), 0, model)); } else { throw new CodecException("Object instance TLV is mandatory for multiple instances object [path:%s]", path); } } else { for (Tlv tlv : tlvs) { if (tlv.getType() != TlvType.OBJECT_INSTANCE) throw new CodecException("Expected TLV of type OBJECT_INSTANCE but was %s [path:%s]", tlv.getType().name(), path); instances.add( parseObjectInstanceTlv(tlv.getChildren(), path.getObjectId(), tlv.getIdentifier(), model)); } } return (T) new LwM2mObject(path.getObjectId(), instances); } // Object instance else if (nodeClass == LwM2mObjectInstance.class) { if (tlvs.length == 1 && tlvs[0].getType() == TlvType.OBJECT_INSTANCE) { if (path.isObjectInstance() && tlvs[0].getIdentifier() != path.getObjectInstanceId()) { throw new CodecException("Id conflict between path [%s] and instance TLV [%d]", path, tlvs[0].getIdentifier()); } // object instance TLV return (T) parseObjectInstanceTlv(tlvs[0].getChildren(), path.getObjectId(), tlvs[0].getIdentifier(), model); } else { // array of TLV resources // try to retrieve the instanceId from the path or the model Integer instanceId = path.getObjectInstanceId(); if (instanceId == null) { // single instance object? ObjectModel oModel = model.getObjectModel(path.getObjectId()); if (oModel != null && !oModel.multiple) { instanceId = 0; } else { instanceId = LwM2mObjectInstance.UNDEFINED; } } return (T) parseObjectInstanceTlv(tlvs, path.getObjectId(), instanceId, model); } } // Resource else if (nodeClass == LwM2mResource.class) { ResourceModel resourceModel = model.getResourceModel(path.getObjectId(), path.getResourceId()); if (tlvs.length == 0 && resourceModel != null && !resourceModel.multiple) { // If there is no TlV value and we know that this resource is a single resource we raise an exception // else we consider this is a multi-instance resource throw new CodecException("TLV payload is mandatory for single resource %s", path); } else if (tlvs.length == 1 && tlvs[0].getType() != TlvType.RESOURCE_INSTANCE) { if (path.isResource() && path.getResourceId() != tlvs[0].getIdentifier()) { throw new CodecException("Id conflict between path [%s] and resource TLV [%s]", path, tlvs[0].getIdentifier()); } return (T) parseResourceTlv(tlvs[0], path.getObjectId(), path.getObjectInstanceId(), model); } else { Type expectedRscType = getResourceType(path, model); return (T) LwM2mMultipleResource.newResource(path.getResourceId(), parseTlvValues(tlvs, expectedRscType, path), expectedRscType); } } else { throw new IllegalArgumentException("invalid node class: " + nodeClass); } } private static LwM2mObjectInstance parseObjectInstanceTlv(Tlv[] rscTlvs, int objectId, int instanceId, LwM2mModel model) throws CodecException { // read resources List<LwM2mResource> resources = new ArrayList<>(rscTlvs.length); for (Tlv rscTlv : rscTlvs) { resources.add(parseResourceTlv(rscTlv, objectId, instanceId, model)); } return new LwM2mObjectInstance(instanceId, resources); } private static LwM2mResource parseResourceTlv(Tlv tlv, int objectId, int objectInstanceId, LwM2mModel model) throws CodecException { LwM2mPath resourcePath = new LwM2mPath(objectId, objectInstanceId, tlv.getIdentifier()); Type expectedType = getResourceType(resourcePath, model); Integer resourceId = tlv.getIdentifier(); switch (tlv.getType()) { case MULTIPLE_RESOURCE: return LwM2mMultipleResource.newResource(resourceId, parseTlvValues(tlv.getChildren(), expectedType, resourcePath), expectedType); case RESOURCE_VALUE: return LwM2mSingleResource.newResource(resourceId, parseTlvValue(tlv.getValue(), expectedType, resourcePath), expectedType); default: throw new CodecException("Invalid TLV type %s for resource %s", tlv.getType(), resourcePath); } } private static Map<Integer, Object> parseTlvValues(Tlv[] tlvs, Type expectedType, LwM2mPath path) throws CodecException { Map<Integer, Object> values = new HashMap<>(); for (Tlv tlvChild : tlvs) { if (tlvChild.getType() != TlvType.RESOURCE_INSTANCE) throw new CodecException("Expected TLV of type RESOURCE_INSTANCE but was %s for path %s", tlvChild.getType().name(), path); values.put(tlvChild.getIdentifier(), parseTlvValue(tlvChild.getValue(), expectedType, path)); } return values; } private static Object parseTlvValue(byte[] value, Type expectedType, LwM2mPath path) throws CodecException { try { LOG.trace("TLV value for path {} and expected type {}: {}", path, expectedType, value); switch (expectedType) { case STRING: return TlvDecoder.decodeString(value); case INTEGER: return TlvDecoder.decodeInteger(value).longValue(); case FLOAT: return TlvDecoder.decodeFloat(value).doubleValue(); case BOOLEAN: return TlvDecoder.decodeBoolean(value); case TIME: return TlvDecoder.decodeDate(value); case OPAQUE: return value; case OBJLNK: return TlvDecoder.decodeObjlnk(value); default: throw new CodecException("Unsupported type %s for path %s", expectedType, path); } } catch (TlvException e) { throw new CodecException(e, "Invalid content [%s] for type %s for path %s", Hex.encodeHexString(value), expectedType, path); } } public static Type getResourceType(LwM2mPath rscPath, LwM2mModel model) throws CodecException { ResourceModel rscDesc = model.getResourceModel(rscPath.getObjectId(), rscPath.getResourceId()); if (rscDesc == null || rscDesc.type == null) { LOG.trace("unknown type for resource : {}", rscPath); // no resource description... opaque return Type.OPAQUE; } else { return rscDesc.type; } } }