diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/attributes/AccessMode.java b/leshan-core/src/main/java/org/eclipse/leshan/core/attributes/AccessMode.java new file mode 100644 index 0000000000..3fa9322025 --- /dev/null +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/attributes/AccessMode.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * Copyright (c) 2013-2018 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 + * Daniel Persson (Husqvarna Group) - Attribute support + *******************************************************************************/ +package org.eclipse.leshan.core.attributes; + +public enum AccessMode { + R, + W, + RW +} diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/attributes/AssignationLevel.java b/leshan-core/src/main/java/org/eclipse/leshan/core/attributes/AssignationLevel.java new file mode 100644 index 0000000000..50cb83a5c5 --- /dev/null +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/attributes/AssignationLevel.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2013-2018 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 + * Daniel Persson (Husqvarna Group) - Attribute support + *******************************************************************************/ +package org.eclipse.leshan.core.attributes; + +/** + * The assignation level of an {@link Attribute}. An attribute can only be applied on one level, + * but it can be assigned on many levels and then be inherited down to its application + * level. + */ +public enum AssignationLevel { + OBJECT, + INSTANCE, + RESOURCE +} diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/attributes/Attachment.java b/leshan-core/src/main/java/org/eclipse/leshan/core/attributes/Attachment.java new file mode 100644 index 0000000000..9977b42209 --- /dev/null +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/attributes/Attachment.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2013-2018 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 + * Daniel Persson (Husqvarna Group) - Attribute support + *******************************************************************************/ +package org.eclipse.leshan.core.attributes; + +/** + * The attachment level of an LwM2m attribute. + * + * This indicates the level (object, instance or resource) where an attribute can + * be applied. E.g. the 'pmin' attribute can only be applied on the Resource level, + * but it can be assigned on all levels. 'pmin' attributes that are assigned to + * the object or instance level are then inherited by all resources that don't have + * their own 'pmin' attribute. + */ +public enum Attachment { + OBJECT, + INSTANCE, + RESOURCE +} diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/attributes/Attribute.java b/leshan-core/src/main/java/org/eclipse/leshan/core/attributes/Attribute.java new file mode 100644 index 0000000000..3c2885fbca --- /dev/null +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/attributes/Attribute.java @@ -0,0 +1,144 @@ +/******************************************************************************* + * Copyright (c) 2013-2018 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 + * Daniel Persson (Husqvarna Group) - Attribute support + *******************************************************************************/ +package org.eclipse.leshan.core.attributes; + +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.eclipse.leshan.util.Validate; + +import java.util.Set; + +/** + * Represents an LwM2m attribute that can be attached to an object, instance or resource. + * + * The {@link Attachment} level of the attribute indicates where it can be applied, e.g. + * the 'pmin' attribute is only applicable to resources, but it can be assigned on all levels + * and then inherited by underlying resources. + */ +public class Attribute { + + public static final String DIMENSION = "dim"; + public static final String OBJECT_VERSION = "ver"; + public static final String MINIMUM_PERIOD = "pmin"; + public static final String MAXIMUM_PERIOD = "pmax"; + public static final String GREATER_THAN = "gt"; + public static final String LESSER_THAN = "lt"; + public static final String STEP = "st"; + + /** + * Metadata container for LwM2m attributes + */ + private static class AttributeModel { + private final String coRELinkParam; + private final Attachment attachment; + private final Set assignationLevels; + private final AccessMode accessMode; + private final Class valueClass; + + private AttributeModel(String coRELinkParam, Attachment attachment, Set assignationLevels, + AccessMode accessMode, Class valueClass) { + this.coRELinkParam = coRELinkParam; + this.attachment = attachment; + this.assignationLevels = assignationLevels; + this.accessMode = accessMode; + this.valueClass = valueClass; + } + } + + private static Map modelMap; + + static { + modelMap = new HashMap<>(); + modelMap.put(DIMENSION, new Attribute.AttributeModel(DIMENSION, Attachment.RESOURCE, EnumSet.of(AssignationLevel.RESOURCE), + AccessMode.R, Long.class)); + modelMap.put(OBJECT_VERSION, new Attribute.AttributeModel(OBJECT_VERSION, Attachment.OBJECT, EnumSet.of(AssignationLevel.OBJECT), + AccessMode.R, String.class)); + modelMap.put(MINIMUM_PERIOD, new Attribute.AttributeModel(MINIMUM_PERIOD, Attachment.RESOURCE, + EnumSet.of(AssignationLevel.OBJECT, AssignationLevel.INSTANCE, AssignationLevel.RESOURCE), + AccessMode.RW, Long.class)); + modelMap.put(MAXIMUM_PERIOD, new Attribute.AttributeModel(MAXIMUM_PERIOD, Attachment.RESOURCE, + EnumSet.of(AssignationLevel.OBJECT, AssignationLevel.INSTANCE, AssignationLevel.RESOURCE), + AccessMode.RW, Long.class)); + modelMap.put(GREATER_THAN, new AttributeModel(GREATER_THAN, Attachment.RESOURCE, + EnumSet.of(AssignationLevel.RESOURCE), AccessMode.RW, Double.class)); + modelMap.put(LESSER_THAN, new AttributeModel(LESSER_THAN, Attachment.RESOURCE, + EnumSet.of(AssignationLevel.RESOURCE), AccessMode.RW, Double.class)); + modelMap.put(STEP, new AttributeModel(STEP, Attachment.RESOURCE, + EnumSet.of(AssignationLevel.RESOURCE), AccessMode.RW, Double.class)); + } + + private final AttributeModel model; + private final Object value; + + public Attribute(String coRELinkParam, Object value) { + Validate.notEmpty(coRELinkParam); + this.model = modelMap.get(coRELinkParam); + if (model == null) { + throw new IllegalArgumentException(String.format("Unsupported attribute '%s'", coRELinkParam)); + } + this.value = ensureMatchingValue(model, value); + } + + public static Attribute create(String coRELinkParam, Object value) { + return new Attribute(coRELinkParam, value); + } + + /** + * Ensures that a provided attribute value matches the attribute value type, including trying + * to perform a correct conversion if the value is a string, e.g. + * @return the converted or original value + */ + private Object ensureMatchingValue(AttributeModel model, Object value) { + // Ensure that the attribute value has the correct type + // If the value is a string, we make an attempt to convert it + Class expectedClass = model.valueClass; + if (!expectedClass.equals(value.getClass()) && value instanceof String) { + if (expectedClass.equals(Long.class)) { + return Long.parseLong(value.toString()); + } else if (expectedClass.equals(Double.class)) { + return Double.parseDouble(value.toString()); + } + } else if (!this.model.valueClass.equals(value.getClass())) { + throw new IllegalArgumentException(String.format("Attribute '%s' must have a value of type %s", + model.coRELinkParam, model.valueClass.getSimpleName())); + } + return value; + } + + public String getCoRELinkParam() { + return model.coRELinkParam; + } + + public Object getValue() { + return value; + } + + public Attachment getAttachment() { + return model.attachment; + } + + public boolean isWritable() { + return model.accessMode == AccessMode.W || model.accessMode == AccessMode.RW; + } + + public boolean canBeAssignedTo(AssignationLevel assignationLevel) { + return model.assignationLevels.contains(assignationLevel); + } +} diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/attributes/AttributeSet.java b/leshan-core/src/main/java/org/eclipse/leshan/core/attributes/AttributeSet.java new file mode 100644 index 0000000000..ac08ea969a --- /dev/null +++ b/leshan-core/src/main/java/org/eclipse/leshan/core/attributes/AttributeSet.java @@ -0,0 +1,157 @@ +/******************************************************************************* + * Copyright (c) 2013-2018 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 + * Daniel Persson (Husqvarna Group) - Attribute support + *******************************************************************************/ +package org.eclipse.leshan.core.attributes; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.eclipse.leshan.util.Validate; + +/** + * A collection of {@link Attribute} instances that are handled as a collection + * that must adhere to rules that are specified in LwM2m, e.g. that the 'pmin' attribute + * must be less than the 'pmax' attribute, if they're both part of the same AttributeSet. + */ +public class AttributeSet { + + private final Map attributeMap = new LinkedHashMap<>(); + + public AttributeSet(Attribute...attributes) { + this(Arrays.asList(attributes)); + } + + public AttributeSet(Collection attributes) { + if (attributes != null && !attributes.isEmpty()) { + for (Attribute attr : attributes) { + // Check for duplicates + if (attributeMap.containsKey(attr.getCoRELinkParam())) { + throw new IllegalArgumentException(String.format("Cannot create attribute set with duplicates (attr: '%s')", + attr.getCoRELinkParam())); + } + attributeMap.put(attr.getCoRELinkParam(), attr); + } + } + } + + /** + * Creates an attribute set from a list of query params. + */ + @Deprecated + public AttributeSet(String[] queryParams) { + for (String param : queryParams) { + String[] keyAndValue = param.split("="); + if (keyAndValue.length != 2) { + throw new IllegalArgumentException(String.format("Cannot parse query param '%s'", param)); + } + Attribute attr = Attribute.create(keyAndValue[0], keyAndValue[1]); + attributeMap.put(attr.getCoRELinkParam(), attr); + } + } + + public void validate(AssignationLevel assignationLevel) { + // Can all attributes be assigned to this level? + for (Attribute attr : attributeMap.values()) { + if (!attr.canBeAssignedTo(assignationLevel)) { + throw new IllegalArgumentException(String.format("Attribute '%s' cannot be assigned to level %s", + attr.getCoRELinkParam(), assignationLevel.name())); + } + } + Attribute pmin = attributeMap.get(Attribute.MINIMUM_PERIOD); + Attribute pmax = attributeMap.get(Attribute.MAXIMUM_PERIOD); + if ((pmin != null) && (pmax != null) && (Long) pmin.getValue() > (Long) pmax.getValue()) { + throw new IllegalArgumentException(String.format("Cannot write attributes where '%s' > '%s'", + pmin.getCoRELinkParam(), pmax.getCoRELinkParam())); + } + } + + + /** + * Returns a new AttributeSet, containing only the attributes that have a matching + * Attachment level. + * @param attachment the Attachment level to filter by + * @return a new {@link AttributeSet} containing the filtered attributes + */ + public AttributeSet filter(Attachment attachment) { + List attrs = new ArrayList<>(); + for (Attribute attr : getAttributes()) { + if (attr.getAttachment() == attachment) { + attrs.add(attr); + } + } + return new AttributeSet(attrs); + } + + /** + * Creates a new AttributeSet by merging another AttributeSet onto this instance. + * @param attributes the AttributeSet that should be merged onto this instance. Attributes in this + * set will overwrite existing attribute values, if present. If this is null, the + * new attribute set will effectively be a clone of the existing one + * @return the merged AttributeSet + */ + public AttributeSet merge(AttributeSet attributes) { + Map merged = new LinkedHashMap<>(); + for (Attribute attr : getAttributes()) { + merged.put(attr.getCoRELinkParam(), attr); + } + if (attributes != null) { + for (Attribute attr : attributes.getAttributes()) { + merged.put(attr.getCoRELinkParam(), attr); + } + } + return new AttributeSet(merged.values()); + } + + /** + * Returns the attributes as a map with the CoRELinkParam as key and the attribute value as map value. + * @return the attributes map + */ + public Map getMap() { + Map result = new LinkedHashMap<>(); + for (Attribute attr : attributeMap.values()) { + result.put(attr.getCoRELinkParam(), attr.getValue()); + } + return Collections.unmodifiableMap(result); + } + + public Collection getAttributes() { + return attributeMap.values(); + } + + public String[] toQueryParams() { + List queries = new LinkedList<>(); + for (Attribute attr : attributeMap.values()) { + queries.add(String.format("%s=%s", attr.getCoRELinkParam(), attr.getValue())); + } + return queries.toArray(new String[queries.size()]); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + String[] queryParams = toQueryParams(); + for (int a = 0;a map = sut.getMap(); + assertEquals("1.1", map.get("ver")); + assertEquals(5L, map.get("pmin")); + assertEquals(60L, map.get("pmax")); + } + + @Test + public void should_merge() { + AttributeSet sut = new AttributeSet( + new Attribute(Attribute.OBJECT_VERSION, "1.1"), + new Attribute(Attribute.MINIMUM_PERIOD, 5L), + new Attribute(Attribute.MAXIMUM_PERIOD, 60L)); + AttributeSet set2 = new AttributeSet( + new Attribute(Attribute.MINIMUM_PERIOD, 10L), + new Attribute(Attribute.MAXIMUM_PERIOD, 120L)); + + AttributeSet merged = sut.merge(set2); + + Map map = merged.getMap(); + assertEquals("1.1", map.get("ver")); + assertEquals(10L, map.get("pmin")); + assertEquals(120L, map.get("pmax")); + + // Assert that the original attribute sets are untouched + map = sut.getMap(); + assertEquals(5L, map.get("pmin")); + map = set2.getMap(); + assertEquals(null, map.get("ver")); + } + + @Test + public void should_to_string() { + AttributeSet sut = new AttributeSet( + new Attribute(Attribute.OBJECT_VERSION, "1.1"), + new Attribute(Attribute.MINIMUM_PERIOD, 5L), + new Attribute(Attribute.MAXIMUM_PERIOD, 60L)); + + assertEquals("ver=1.1&pmin=5&pmax=60", sut.toString()); + } + + @Test + public void should_throw_on_duplicates() { + exception.expect(IllegalArgumentException.class); + new AttributeSet( + new Attribute(Attribute.OBJECT_VERSION, "1.1"), + new Attribute(Attribute.MINIMUM_PERIOD, 5L), + new Attribute(Attribute.MINIMUM_PERIOD, 60L)); + } + + @Test + public void should_validate_assignation() { + AttributeSet sut = new AttributeSet( + new Attribute(Attribute.MINIMUM_PERIOD, 5L), + new Attribute(Attribute.MAXIMUM_PERIOD, 60L)); + Collection attributes = sut.getAttributes(); + assertEquals(2, attributes.size()); + sut.validate(AssignationLevel.RESOURCE); + } + + @Test + public void should_throw_on_invalid_assignation_level() { + AttributeSet sut = new AttributeSet( + new Attribute(Attribute.OBJECT_VERSION, "1.1"), + new Attribute(Attribute.MINIMUM_PERIOD, 5L), + new Attribute(Attribute.MAXIMUM_PERIOD, 60L)); + + exception.expect(IllegalArgumentException.class); + // OBJECT_VERSION cannot be assigned on resource level + sut.validate(AssignationLevel.RESOURCE); + } + + @Test + public void should_throw_on_invalid_pmin_pmax() { + AttributeSet sut = new AttributeSet( + new Attribute(Attribute.MINIMUM_PERIOD, 50L), + new Attribute(Attribute.MAXIMUM_PERIOD, 49L)); + + exception.expect(IllegalArgumentException.class); + // pmin cannot be greater then pmax + sut.validate(AssignationLevel.RESOURCE); + } +} diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/attributes/AttributeTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/attributes/AttributeTest.java new file mode 100644 index 0000000000..1677568856 --- /dev/null +++ b/leshan-core/src/test/java/org/eclipse/leshan/core/attributes/AttributeTest.java @@ -0,0 +1,31 @@ +package org.eclipse.leshan.core.attributes; + +import static org.junit.Assert.*; + +import java.util.EnumSet; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class AttributeTest { + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void should_pick_correct_model() { + Attribute verAttribute = new Attribute(Attribute.OBJECT_VERSION, "1.0"); + assertEquals("ver", verAttribute.getCoRELinkParam()); + assertEquals("1.0", verAttribute.getValue()); + assertTrue(verAttribute.canBeAssignedTo(AssignationLevel.OBJECT)); + assertFalse(verAttribute.isWritable()); + } + + @Test + public void should_throw_on_invalid_value_type() { + exception.expect(IllegalArgumentException.class); + exception.expectMessage(Attribute.OBJECT_VERSION); + new Attribute(Attribute.OBJECT_VERSION, 123); + } +}