diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/util/LinkFormatHelper.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/util/LinkFormatHelper.java index f6a3381c05..384313b23a 100644 --- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/util/LinkFormatHelper.java +++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/util/LinkFormatHelper.java @@ -48,8 +48,8 @@ public static Link[] getClientDescription(Collection objectE // create links for "object" String rootURL = getPath("/", root); - Map attributes = new HashMap<>(); - attributes.put("rt", "oma.lwm2m"); + Map attributes = new HashMap<>(); + attributes.put("rt", "\"oma.lwm2m\""); links.add(new Link(rootURL, attributes)); // sort resources @@ -67,7 +67,7 @@ public int compare(LwM2mObjectEnabler o1, LwM2mObjectEnabler o2) { List availableInstance = objectEnabler.getAvailableInstanceIds(); // Include an object link if there are no instances or there are object attributes (e.g. "ver") - Map objectAttributes = getObjectAttributes(objectEnabler.getObjectModel()); + Map objectAttributes = getObjectAttributes(objectEnabler.getObjectModel()); if (availableInstance.isEmpty() || (objectAttributes != null)) { String objectInstanceUrl = getPath("/", root, Integer.toString(objectEnabler.getId())); links.add(new Link(objectInstanceUrl, objectAttributes)); @@ -89,7 +89,7 @@ public static Link[] getObjectDescription(LwM2mObjectEnabler objectEnabler, Stri String rootPath = root == null ? "" : root; // create link for "object" - Map objectAttributes = getObjectAttributes(objectEnabler.getObjectModel()); + Map objectAttributes = getObjectAttributes(objectEnabler.getObjectModel()); String objectURL = getPath("/", rootPath, Integer.toString(objectEnabler.getId())); links.add(new Link(objectURL, objectAttributes)); @@ -192,7 +192,7 @@ private static String normalize(String input, int len, int off) { return sb.toString(); } - private static Map getObjectAttributes(ObjectModel objectModel) { + private static Map getObjectAttributes(ObjectModel objectModel) { if (StringUtils.isEmpty(objectModel.version) || ObjectModel.DEFAULT_VERSION.equals(objectModel.version)) { return null; } diff --git a/leshan-client-core/src/test/java/org/eclipse/leshan/client/util/LinkFormatHelperTest.java b/leshan-client-core/src/test/java/org/eclipse/leshan/client/util/LinkFormatHelperTest.java index 478e1e9b69..9984abba5f 100644 --- a/leshan-client-core/src/test/java/org/eclipse/leshan/client/util/LinkFormatHelperTest.java +++ b/leshan-client-core/src/test/java/org/eclipse/leshan/client/util/LinkFormatHelperTest.java @@ -85,8 +85,7 @@ public void encode_objectModel_to_linkObject_with_version2_0() { Link[] links = LinkFormatHelper.getObjectDescription(createObjectEnabler(locationModel), "/"); String strLinks = Link.serialize(links); - assertEquals(";ver=\"2.0\",,,,,,,,", - strLinks); + assertEquals(";ver=2.0,,,,,,,,", strLinks); } @Test @@ -128,7 +127,7 @@ public void encode_client_description_with_version_2_0() { Link[] links = LinkFormatHelper.getClientDescription(objectEnablers, null); String strLinks = Link.serialize(links); - assertEquals(";rt=\"oma.lwm2m\",;ver=\"2.0\",,", strLinks); + assertEquals(";rt=\"oma.lwm2m\",;ver=2.0,,", strLinks); } @Test @@ -142,7 +141,7 @@ public void encode_client_description_with_version_2_0_no_instances() { Link[] links = LinkFormatHelper.getClientDescription(objectEnablers, null); String strLinks = Link.serialize(links); - assertEquals(";rt=\"oma.lwm2m\",;ver=\"2.0\"", strLinks); + assertEquals(";rt=\"oma.lwm2m\",;ver=2.0", strLinks); } private ObjectModel getObjectModel(int id) { diff --git a/leshan-core/src/main/java/org/eclipse/leshan/Link.java b/leshan-core/src/main/java/org/eclipse/leshan/Link.java index ef633ecc76..41a839b795 100644 --- a/leshan-core/src/main/java/org/eclipse/leshan/Link.java +++ b/leshan-core/src/main/java/org/eclipse/leshan/Link.java @@ -24,6 +24,7 @@ import java.util.Map.Entry; import org.eclipse.leshan.util.StringUtils; +import org.eclipse.leshan.util.Validate; /** * A Link as defined in http://tools.ietf.org/html/rfc6690. @@ -46,7 +47,7 @@ public class Link implements Serializable { private final String url; - private final Map attributes; + private final Map attributes; /** * Creates a new Link without attributes. @@ -54,7 +55,7 @@ public class Link implements Serializable { * @param url the object link URL */ public Link(String url) { - this(url, null); + this(url, (Map) null); } /** @@ -63,7 +64,8 @@ public Link(String url) { * @param url the link URL * @param attributes the object link attributes or null if the link has no attributes */ - public Link(String url, Map attributes) { + public Link(String url, Map attributes) { + Validate.notNull(url); this.url = url; if (attributes != null) { this.attributes = Collections.unmodifiableMap(new HashMap<>(attributes)); @@ -72,6 +74,61 @@ public Link(String url, Map attributes) { } } + /** + * Creates a new link and with its attributes. + * + * @param url the link URL + * @param attributes the object link attributes or null if the link has no attributes + */ + @SuppressWarnings("unchecked") + public Link(String url, Map attributes, Class clazz) { + Validate.notNull(url); + this.url = url; + if (attributes == null || attributes.isEmpty()) { + this.attributes = Collections.emptyMap(); + } else { + if (String.class.equals(clazz)) { + this.attributes = Collections.unmodifiableMap((Map) new HashMap<>(attributes)); + } else { + HashMap attributesMap = new HashMap<>(); + for (Entry attr : attributes.entrySet()) { + if (attr.getValue() == null) { + attributesMap.put(attr.getKey(), null); + } else { + attributesMap.put(attr.getKey(), attr.getValue().toString()); + } + + } + this.attributes = Collections.unmodifiableMap(attributesMap); + } + } + } + + /** + * Creates a new link and with its attributes. + * + * @param url the link URL + * @param attributes the object link attributes. The format is attributeKey1, attributeValue1, attributeKey2, + * attributeValue2. For empty attributes null value should be used. + */ + public Link(String url, String... attributes) { + Validate.notNull(url); + this.url = url; + if (attributes == null || attributes.length == 0) { + this.attributes = Collections.emptyMap(); + } else { + if (attributes.length % 2 != 0) { + throw new IllegalArgumentException("Each attributes key must have a value"); + } + + HashMap attributesMap = new HashMap<>(); + for (int i = 0; i < attributes.length; i = i + 2) { + attributesMap.put(attributes[i], attributes[i + 1]); + } + this.attributes = Collections.unmodifiableMap(attributesMap); + } + } + public String getUrl() { return url; } @@ -81,7 +138,7 @@ public String getUrl() { * * @return an unmodifiable map containing the link attributes */ - public Map getAttributes() { + public Map getAttributes() { return attributes; } @@ -92,18 +149,14 @@ public String toString() { builder.append(getUrl()); builder.append('>'); - Map attributes = getAttributes(); + Map attributes = getAttributes(); if (attributes != null && !attributes.isEmpty()) { - for (Entry entry : attributes.entrySet()) { + for (Entry entry : attributes.entrySet()) { builder.append(";"); builder.append(entry.getKey()); if (entry.getValue() != null) { builder.append("="); - if (entry.getValue() instanceof String) { - builder.append("\"").append(entry.getValue()).append("\""); - } else { - builder.append(entry.getValue()); - } + builder.append(entry.getValue()); } } } @@ -132,22 +185,16 @@ public static Link[] parse(byte[] content) { url = StringUtils.removeStart(StringUtils.removeEnd(url, ">"), "<"); // parse attributes - Map attributes = new HashMap<>(); + Map attributes = new HashMap<>(); if (linkParts.length > 1) { for (int i = 1; i < linkParts.length; i++) { String[] attParts = linkParts[i].split("="); if (attParts.length > 0) { String key = attParts[0]; - Object value = null; + String value = null; if (attParts.length > 1) { - String rawvalue = attParts[1]; - try { - value = Integer.valueOf(rawvalue); - } catch (NumberFormatException e) { - - value = rawvalue.replaceFirst("^\"(.*)\"$", "$1"); - } + value = attParts[1]; } attributes.put(key, value); } @@ -210,4 +257,18 @@ public boolean equals(Object obj) { return false; return true; } + + /** + * remove quote from string, only if it begins and ends by a quote. + * + * @param string to unquote + * @return unquoted string or the original string if there no quote to remove. + */ + public static String unquote(String string) { + if (string != null && string.length() >= 2 && string.charAt(0) == '"' + && string.charAt(string.length() - 1) == '"') { + string = string.substring(1, string.length() - 1); + } + return string; + } } \ No newline at end of file diff --git a/leshan-core/src/test/java/org/eclipse/leshan/LinkObjectTest.java b/leshan-core/src/test/java/org/eclipse/leshan/LinkObjectTest.java index ce47903bd0..67bfabe8e4 100644 --- a/leshan-core/src/test/java/org/eclipse/leshan/LinkObjectTest.java +++ b/leshan-core/src/test/java/org/eclipse/leshan/LinkObjectTest.java @@ -15,6 +15,8 @@ *******************************************************************************/ package org.eclipse.leshan; +import static org.junit.Assert.assertEquals; + import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -30,9 +32,9 @@ public void parse_with_some_attributes() { Assert.assertEquals(5, parse.length); Assert.assertEquals("/", parse[0].getUrl()); - Map attResult = new HashMap<>(); - attResult.put("rt", "oma.lwm2m"); - attResult.put("ct", 100); + Map attResult = new HashMap<>(); + attResult.put("rt", "\"oma.lwm2m\""); + attResult.put("ct", "100"); Assert.assertEquals(attResult, parse[0].getAttributes()); Assert.assertEquals("/1/101", parse[1].getUrl()); @@ -58,7 +60,7 @@ public void parse_with_quoted_attributes() { Assert.assertEquals("/", parse[0].getUrl()); Map attResult = new HashMap<>(); - attResult.put("k1", "quotes\"inside"); + attResult.put("k1", "\"quotes\"inside\""); attResult.put("k2", "endwithquotes\""); attResult.put("k3", "noquotes"); attResult.put("k4", "\"startwithquotes"); @@ -79,17 +81,9 @@ public void serialyse_without_attribute() { @Test public void serialyse_with_attributes() { - HashMap attributesObj1 = new HashMap<>(); - attributesObj1.put("number", 12); - Link obj1 = new Link("/1/0/1", attributesObj1); - - HashMap attributesObj2 = new HashMap<>(); - attributesObj2.put("string", "stringval"); - Link obj2 = new Link("/2/1", attributesObj2); - - HashMap attributesObj3 = new HashMap<>(); - attributesObj3.put("empty", null); - Link obj3 = new Link("/3", attributesObj3); + Link obj1 = new Link("/1/0/1", "number", "12"); + Link obj2 = new Link("/2/1", "string", "\"stringval\""); + Link obj3 = new Link("/3", "empty", null); String res = Link.serialize(obj1, obj2, obj3); @@ -99,9 +93,9 @@ public void serialyse_with_attributes() { @Test public void serialyse_with_root_url() { - HashMap attributesObj1 = new HashMap<>(); + HashMap attributesObj1 = new HashMap<>(); attributesObj1.put("number", 12); - Link obj1 = new Link("/", attributesObj1); + Link obj1 = new Link("/", attributesObj1, Integer.class); String res = Link.serialize(obj1); @@ -115,11 +109,11 @@ public void serialyse_then_parse_with_severals_attributes() { attributesObj1.put("number1", 1); attributesObj1.put("number2", 1); attributesObj1.put("string1", "stringval1"); - Link obj1 = new Link("/1/0", attributesObj1); + Link obj1 = new Link("/1/0", attributesObj1, Object.class); - HashMap attributesObj2 = new HashMap<>(); + HashMap attributesObj2 = new HashMap<>(); attributesObj2.put("number3", 3); - Link obj2 = new Link("/2", attributesObj2); + Link obj2 = new Link("/2", attributesObj2, Integer.class); Link[] input = new Link[] { obj1, obj2 }; String strObjs = Link.serialize(input); @@ -137,4 +131,26 @@ public void parse_then_serialyse_with_rt_attribute() { Assert.assertEquals(input, output); } + + @Test + public void parse_quoted_ver_attributes() { + String input = ";ver=\"2.2\""; + Link[] objs = Link.parse(input.getBytes()); + assertEquals(objs[0].getAttributes().get("ver"), "\"2.2\""); + } + + @Test + public void parse_unquoted_ver_attributes() { + String input = ";ver=2.2"; + Link[] objs = Link.parse(input.getBytes()); + assertEquals(objs[0].getAttributes().get("ver"), "2.2"); + } + + @Test + public void serialyse_ver_attributes_without_quote() { + Map att = new HashMap<>(); + att.put("ver", "2.2"); + Link link = new Link("/1", att); + assertEquals(";ver=2.2", Link.serialize(link)); + } } diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilderTest.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilderTest.java index 0f4db71ccc..4a314e741d 100644 --- a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilderTest.java +++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilderTest.java @@ -78,7 +78,7 @@ private Registration newRegistration(String rootpath) throws UnknownHostExceptio Identity.unsecure(Inet4Address.getLoopbackAddress(), 12354)); if (rootpath != null) { Map attr = new HashMap<>(); - attr.put("rt", "oma.lwm2m"); + attr.put("rt", "\"oma.lwm2m\""); b.objectLinks(new Link[] { new Link(rootpath, attr) }); } return b.build(); diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/Registration.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/Registration.java index b06e2f6242..b206b893c9 100644 --- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/Registration.java +++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/registration/Registration.java @@ -98,7 +98,7 @@ protected Registration(String id, String endpoint, Identity identity, String lwM String rootPath = "/"; if (objectLinks != null) { for (Link link : objectLinks) { - if (link != null && "oma.lwm2m".equals(link.getAttributes().get("rt"))) { + if (link != null && "oma.lwm2m".equals(Link.unquote(link.getAttributes().get("rt")))) { rootPath = link.getUrl(); break; } @@ -366,21 +366,23 @@ public static Map getSupportedObject(String rootPath, Link[] ob try { // extract object id and version int objectId = Integer.parseInt(m.group(1)); - Object version = link.getAttributes().get(Attribute.OBJECT_VERSION); + String version = link.getAttributes().get(Attribute.OBJECT_VERSION); + // un-quote version (see https://github.com/eclipse/leshan/issues/732) + version = Link.unquote(version); String currentVersion = objects.get(objectId); // store it in map if (currentVersion == null) { // we never find version for this object add it - if (version instanceof String) { - objects.put(objectId, (String) version); + if (version != null) { + objects.put(objectId, version); } else { objects.put(objectId, ObjectModel.DEFAULT_VERSION); } } else { // if version is already set, we override it only if new version is not DEFAULT_VERSION - if (version instanceof String && !version.equals(ObjectModel.DEFAULT_VERSION)) { - objects.put(objectId, (String) version); + if (version != null && !version.equals(ObjectModel.DEFAULT_VERSION)) { + objects.put(objectId, version); } } } catch (NumberFormatException e) { diff --git a/leshan-server-core/src/test/java/org/eclipse/leshan/server/registration/RegistrationTest.java b/leshan-server-core/src/test/java/org/eclipse/leshan/server/registration/RegistrationTest.java index ac289c8b59..ae7af2779b 100644 --- a/leshan-server-core/src/test/java/org/eclipse/leshan/server/registration/RegistrationTest.java +++ b/leshan-server-core/src/test/java/org/eclipse/leshan/server/registration/RegistrationTest.java @@ -52,6 +52,17 @@ public void test_supported_object_given_an_object_link_with_rootpath() { assertNull(supportedObject.get(3)); } + @Test + public void test_supported_object_given_an_object_link_with_unquoted_rootpath() { + Registration reg = given_a_registration_with_object_link_like(";rt=oma.lwm2m, ,"); + + // Ensure supported objects are correct + Map supportedObject = reg.getSupportedObject(); + assertEquals(1, supportedObject.size()); + assertEquals(ObjectModel.DEFAULT_VERSION, supportedObject.get(1)); + assertNull(supportedObject.get(3)); + } + @Test public void test_supported_object_given_an_object_link_with_regexp_rootpath() { Registration reg = given_a_registration_with_object_link_like( diff --git a/leshan-server-redis/src/main/java/org/eclipse/leshan/server/redis/serialization/RegistrationSerDes.java b/leshan-server-redis/src/main/java/org/eclipse/leshan/server/redis/serialization/RegistrationSerDes.java index b189fbe109..d864de1b46 100644 --- a/leshan-server-redis/src/main/java/org/eclipse/leshan/server/redis/serialization/RegistrationSerDes.java +++ b/leshan-server-redis/src/main/java/org/eclipse/leshan/server/redis/serialization/RegistrationSerDes.java @@ -51,11 +51,9 @@ public static JsonObject jSerialize(Registration r) { JsonObject ol = Json.object(); ol.add("url", l.getUrl()); JsonObject at = Json.object(); - for (Map.Entry e : l.getAttributes().entrySet()) { + for (Map.Entry e : l.getAttributes().entrySet()) { if (e.getValue() == null) { at.add(e.getKey(), Json.NULL); - } else if (e.getValue() instanceof Integer) { - at.add(e.getKey(), (int) e.getValue()); } else { at.add(e.getKey(), e.getValue().toString()); } @@ -99,14 +97,15 @@ public static Registration deserialize(JsonObject jObj) { for (int i = 0; i < links.size(); i++) { JsonObject ol = (JsonObject) links.get(i); - Map attMap = new HashMap<>(); + Map attMap = new HashMap<>(); JsonObject att = (JsonObject) ol.get("at"); for (String k : att.names()) { JsonValue jsonValue = att.get(k); if (jsonValue.isNull()) { attMap.put(k, null); } else if (jsonValue.isNumber()) { - attMap.put(k, jsonValue.asInt()); + // This else block is just needed for retro-compatibility + attMap.put(k, Integer.toString(jsonValue.asInt())); } else { attMap.put(k, jsonValue.asString()); } diff --git a/leshan-server-redis/src/test/java/org/eclipse/leshan/server/redis/serialization/RegistrationSerDesTest.java b/leshan-server-redis/src/test/java/org/eclipse/leshan/server/redis/serialization/RegistrationSerDesTest.java index 5cba1dd5c6..426083e9a3 100644 --- a/leshan-server-redis/src/test/java/org/eclipse/leshan/server/redis/serialization/RegistrationSerDesTest.java +++ b/leshan-server-redis/src/test/java/org/eclipse/leshan/server/redis/serialization/RegistrationSerDesTest.java @@ -36,7 +36,7 @@ public void ser_and_des_are_equals() throws Exception { att.put("ts", 12); att.put("rt", "test"); att.put("hb", null); - objs[0] = new Link("/0/1024/2", att); + objs[0] = new Link("/0/1024/2", att, Object.class); objs[1] = new Link("/0/2"); Registration.Builder builder = new Registration.Builder("registrationId", "endpoint",