diff --git a/californium-core/src/main/java/org/eclipse/californium/core/CoapResource.java b/californium-core/src/main/java/org/eclipse/californium/core/CoapResource.java index bc186dbd..436945d9 100644 --- a/californium-core/src/main/java/org/eclipse/californium/core/CoapResource.java +++ b/californium-core/src/main/java/org/eclipse/californium/core/CoapResource.java @@ -20,6 +20,7 @@ package org.eclipse.californium.core; import java.net.URI; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -33,6 +34,7 @@ import org.eclipse.californium.core.coap.CoAP.Code; import org.eclipse.californium.core.coap.CoAP.ResponseCode; import org.eclipse.californium.core.coap.CoAP.Type; +import org.eclipse.californium.core.coap.OptionSet; import org.eclipse.californium.core.coap.Response; import org.eclipse.californium.core.network.Endpoint; import org.eclipse.californium.core.network.Exchange; @@ -40,7 +42,11 @@ import org.eclipse.californium.core.observe.ObserveRelation; import org.eclipse.californium.core.observe.ObserveRelationContainer; import org.eclipse.californium.core.server.ServerMessageDeliverer; +import org.eclipse.californium.core.server.resources.AcceptDefaultSupport; +import org.eclipse.californium.core.server.resources.AcceptSupport; import org.eclipse.californium.core.server.resources.CoapExchange; +import org.eclipse.californium.core.server.resources.ETagDefaultSupport; +import org.eclipse.californium.core.server.resources.ETagSupport; import org.eclipse.californium.core.server.resources.Resource; import org.eclipse.californium.core.server.resources.ResourceAttributes; import org.eclipse.californium.core.server.resources.ResourceObserver; @@ -159,6 +165,12 @@ public class CoapResource implements Resource { /* The notification orderer. */ private ObserveNotificationOrderer notificationOrderer; + /* Support for ETags */ + private ETagSupport etagSupport; + + /* Support for Accept */ + private AcceptSupport acceptSupport; + /** * Constructs a new resource with the specified name. * @@ -184,8 +196,9 @@ public CoapResource(String name, boolean visible) { this.observers = new CopyOnWriteArrayList(); this.observeRelations = new ObserveRelationContainer(); this.notificationOrderer = new ObserveNotificationOrderer(); + this.etagSupport = createETagSupport(); + this.acceptSupport = createAcceptSupport(); } - /** * Handles any request in the given exchange. By default it responds @@ -201,6 +214,13 @@ public CoapResource(String name, boolean visible) { @Override public void handleRequest(final Exchange exchange) { Code code = exchange.getRequest().getCode(); + + if (code == Code.GET && validateETag(exchange)) + return; + + if (expectsUnacceptableFormat(exchange)) + return; + switch (code) { case GET: handleGET(new CoapExchange(exchange, this)); break; case POST: handlePOST(new CoapExchange(exchange, this)); break; @@ -209,6 +229,57 @@ public void handleRequest(final Exchange exchange) { } } + /** + * Compares the ETags of the request with the current ETag from the + * {@link ETagSupport}. If an ETag matches, the method automatically + * responds with a 2.03 (Valid) and returns true. If there is no ETag or if + * there is not ETagSupport object, the method returns false. + * + * @param exchange the exchange + * @return true, if the request has a valid ETag, false otherwise + */ + public boolean validateETag(Exchange exchange) { + ETagSupport support = getETagSupport(); + OptionSet options = exchange.getRequest().getOptions(); + if (options.getETagCount() > 0 && support != null) { + byte[] current = support.getCurrentETag(); + for (byte[] etag:options.getETags()) + if (Arrays.equals(etag, current)) { + Response response = new Response(ResponseCode.VALID); + response.getOptions().addETag(current); + exchange.sendResponse(response); + return true; + } + } + return false; + } + + /** + * Compares the accept value of the request with the accept values of the + * AcceptSupport object (ASO). If the request has no accept value, it is + * considered to accept anything. If there is no ASO, the resource is + * considered to accept any request (it still can send a Not-Acceptable + * response). Only if the request has an accept value and the ASO does not + * accept it, this method responds with a 4.06 (Not Acceptable) and returns + * true. The {@link AcceptDefaultSupport} by default accepts everything. + * + * @param exchange the exchange + * @return true, if the request does NOT accept the content format of this + * resource + */ + public boolean expectsUnacceptableFormat(Exchange exchange) { + AcceptSupport support = getAcceptSupport(); + OptionSet options = exchange.getRequest().getOptions(); + if (options.hasAccept() && support != null) { + if (! support.isAcceptable(options.getAccept())) { + Response response = new Response(ResponseCode.NOT_ACCEPTABLE); + exchange.sendResponse(response); + return true; + } + } + return false; + } + /** * Handles the GET request in the given CoAPExchange. By default it * responds with a 4.05 (Method Not Allowed). Override this method to @@ -677,7 +748,7 @@ public void removeObserveRelation(ObserveRelation relation) { } /** - * Returns the number of observe realtions that this resource has to CoAP + * Returns the number of observe relations that this resource has to CoAP * clients. * * @return the observer count @@ -718,6 +789,60 @@ protected void notifyObserverRelations() { relation.notifyObservers(); } } + + /** + * Creates a new ETagSupport object. + * + * @return an ETagSupport object + */ + protected ETagSupport createETagSupport() { + return new ETagDefaultSupport(); + } + + /** + * Creates a new AcceptSupport object. + * + * @return an AcceptSupport object + */ + protected AcceptSupport createAcceptSupport() { + return new AcceptDefaultSupport(); + } + + /** + * Gets the ETagSupport object or null if none is present. + * + * @return the ETagSupport object + */ + public ETagSupport getETagSupport() { + return etagSupport; + } + + /** + * Sets the ETag support object. + * + * @param support the new ETag support object + */ + public void setETagSupport(ETagSupport support) { + this.etagSupport = support; + } + + /** + * Gets the AcceptSupport object or null if none is present. + * + * @return the AcceptSupport object + */ + public AcceptSupport getAcceptSupport() { + return acceptSupport; + } + + /** + * Sets the accept support object. + * + * @param support the new accept support obejct + */ + public void setAcceptSupport(AcceptSupport support) { + this.acceptSupport = support; + } /* (non-Javadoc) * @see org.eclipse.californium.core.server.resources.Resource#getChildren() diff --git a/californium-core/src/main/java/org/eclipse/californium/core/server/resources/AcceptDefaultSupport.java b/californium-core/src/main/java/org/eclipse/californium/core/server/resources/AcceptDefaultSupport.java new file mode 100644 index 00000000..2e8d5cef --- /dev/null +++ b/californium-core/src/main/java/org/eclipse/californium/core/server/resources/AcceptDefaultSupport.java @@ -0,0 +1,54 @@ +package org.eclipse.californium.core.server.resources; + +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * The default implementation for supporting accept options in resources. An + * instance of AcceptDefaultSupport contains a set with all acceptable content + * formats. If it holds no value at all, then ALL content formats are + * considered accepted. + */ +public class AcceptDefaultSupport implements AcceptSupport { + + /** The acceptable content formats. */ + private Set acceptables; + + /** + * Instantiates a support object for accept options. + */ + public AcceptDefaultSupport() { + acceptables = new CopyOnWriteArraySet(); + } + + /* (non-Javadoc) + * @see org.eclipse.californium.core.server.resources.AcceptSupport#addAcceptable(int) + */ + public void addAcceptable(int... acceptableValues) { + for (int acc:acceptableValues) + acceptables.add(acc); + } + + /* (non-Javadoc) + * @see org.eclipse.californium.core.server.resources.AcceptSupport#removeAcceptable(int) + */ + public void removeAcceptable(int... values) { + for (int val:values) + acceptables.remove(val); + } + + /* (non-Javadoc) + * @see org.eclipse.californium.core.server.resources.AcceptSupport#accepts(int) + */ + public boolean isAcceptable(int acceptable) { + return acceptables.isEmpty() || acceptables.contains(acceptable); + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "AcceptDefaultSupport("+acceptables.toString()+")"; + } +} diff --git a/californium-core/src/main/java/org/eclipse/californium/core/server/resources/AcceptSupport.java b/californium-core/src/main/java/org/eclipse/californium/core/server/resources/AcceptSupport.java new file mode 100644 index 00000000..eb634bb0 --- /dev/null +++ b/californium-core/src/main/java/org/eclipse/californium/core/server/resources/AcceptSupport.java @@ -0,0 +1,32 @@ +package org.eclipse.californium.core.server.resources; + +/** + * An AcceptSupport object supports a resource in checking if the requested + * content format (accept option) of a request is consistent with the content + * format(s) the resource provides. + */ +public interface AcceptSupport { + + /** + * Adds the specified values to the acceptable content format requests. + * + * @param acceptableValues the acceptable values + */ + public void addAcceptable(int... acceptableValues); + + /** + * Removes the specified values from the acceptable content format requests. + * + * @param values the values + */ + public void removeAcceptable(int... values); + + /** + * Returns true if the specified content format can be provided. + * + * @param acceptable the requested content format + * @return true, if the accept request can be met + */ + public boolean isAcceptable(int acceptable); + +} diff --git a/californium-core/src/main/java/org/eclipse/californium/core/server/resources/ETagDefaultSupport.java b/californium-core/src/main/java/org/eclipse/californium/core/server/resources/ETagDefaultSupport.java new file mode 100644 index 00000000..49ffb3d4 --- /dev/null +++ b/californium-core/src/main/java/org/eclipse/californium/core/server/resources/ETagDefaultSupport.java @@ -0,0 +1,123 @@ +package org.eclipse.californium.core.server.resources; + +import java.util.Random; +import java.util.concurrent.atomic.AtomicLong; + +/** + * This is e default ETag support implementation for a resource. An instance of + * this class holds the current ETag. If the content of a resource changes, it + * should call {@link #nextETag()} to increase the ETag value by 1. This class + * is thread-safe. + */ +public class ETagDefaultSupport implements ETagSupport { + + /** The maximum size allowed by the coap-18 draft. */ + public static final int MAX_BYTES = 8; + + /** The minimum size allowed by the coap-18 draft. */ + public static final int MIN_BYTES = 0; + + /** A static instance of random to set random initial ETag values. */ + private static final Random rand = new Random(); + + /** The size of the ETag. */ + private int bytes; + + /** The current ETag value. */ + private AtomicLong current; + + /** + * Instantiates a new instance of ETagDefaultSupport that generates 8 bytes + * and starts with a random value. long ETags. + */ + public ETagDefaultSupport() { + this(MAX_BYTES); + } + + /** + * Instantiates a new instance of ETagDefaultSupport that generates ETags of + * the specified size and starts with a random value. + * + * @param bytes the size of the ETags + */ + public ETagDefaultSupport(int bytes) { + this(bytes, rand.nextLong()); + } + + /** + * Instantiates a new instance of ETagDefaultSupport that generates ETags of + * the specified size and starts with the specified value. + * + * @param bytes the size of the ETags + * @param initialValue the initial value + */ + public ETagDefaultSupport(int bytes, long initialValue) { + current = new AtomicLong(initialValue); + if (bytes > MAX_BYTES) + throw new IllegalArgumentException("ETags must not be longer than 8 bytes but is "+bytes); + if (bytes < MIN_BYTES) + throw new IllegalArgumentException("ETags must be at least 0 bytes long but is "+bytes); + this.bytes = bytes; + } + + /* (non-Javadoc) + * @see org.eclipse.californium.core.server.resources.ETagSupport#nextETag() + */ + public byte[] nextETag() { + return long2bytes(current.incrementAndGet(), bytes); + } + + /* (non-Javadoc) + * @see org.eclipse.californium.core.server.resources.ETagSupport#getCurrentETag() + */ + public byte[] getCurrentETag() { + return long2bytes(current.get(), bytes); + } + + /* (non-Javadoc) + * @see org.eclipse.californium.core.server.resources.ETagSupport#setCurrentETag(byte[]) + */ + public void setCurrentETag(byte[] etag) { + long cur = 0; + for (int i=0;i>> (length-1)*8) & 0xFF) == 0 && length > 0;length--); + byte[] etag = new byte[length]; + for (int i=0;i>> i*8); + } + return etag; + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + public String toString() { + return "DefaultETagSupport(current: 0x"+getCurrentETagAsString()+", bytes: "+bytes+")"; + } + +} diff --git a/californium-core/src/main/java/org/eclipse/californium/core/server/resources/ETagSupport.java b/californium-core/src/main/java/org/eclipse/californium/core/server/resources/ETagSupport.java new file mode 100644 index 00000000..a50fbe2a --- /dev/null +++ b/californium-core/src/main/java/org/eclipse/californium/core/server/resources/ETagSupport.java @@ -0,0 +1,37 @@ +package org.eclipse.californium.core.server.resources; + +/** + * An ETag Support object supports a resource in generating and maintaining + * ETags. + */ +public interface ETagSupport { + + /** + * Generates the next ETag. + * + * @return the ETag as byte array + */ + public byte[] nextETag(); + + /** + * Gets the current ETag. + * + * @return the current ETag + */ + public byte[] getCurrentETag(); + + /** + * Sets the current ETag. + * + * @param etag the new ETag + */ + public void setCurrentETag(byte[] etag); + + /** + * Gets the current ETag as string. + * + * @return the current ETag as string + */ + public String getCurrentETagAsString(); + +} \ No newline at end of file diff --git a/californium-core/src/test/java/org/eclipse/californium/core/test/AcceptSupportResourceTest.java b/californium-core/src/test/java/org/eclipse/californium/core/test/AcceptSupportResourceTest.java new file mode 100644 index 00000000..f1f8b83b --- /dev/null +++ b/californium-core/src/test/java/org/eclipse/californium/core/test/AcceptSupportResourceTest.java @@ -0,0 +1,84 @@ +package org.eclipse.californium.core.test; + +import org.eclipse.californium.core.CoapClient; +import org.eclipse.californium.core.CoapResource; +import org.eclipse.californium.core.CoapResponse; +import org.eclipse.californium.core.CoapServer; +import org.eclipse.californium.core.coap.CoAP.ResponseCode; +import org.eclipse.californium.core.coap.MediaTypeRegistry; +import org.eclipse.californium.core.network.CoAPEndpoint; +import org.eclipse.californium.core.server.resources.CoapExchange; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class AcceptSupportResourceTest { + + public static final String RESPONSE_PAYLOAD = "some content"; + public static final String TARGET = "test"; + + private CoapServer server; + private int serverPort; + + private TestResource testResource; + + @Before + public void startupServer() throws Exception { + System.out.println("\nStart "+getClass().getSimpleName()); + + CoAPEndpoint serverEndpoint = new CoAPEndpoint(); + server = new CoapServer(); + server.addEndpoint(serverEndpoint); + server.add(testResource = new TestResource(TARGET)); + server.start(); + + serverPort = serverEndpoint.getAddress().getPort(); + } + + @After + public void shutdownServer() { + server.destroy(); + System.out.println("End "+getClass().getSimpleName()); + } + + @Test + public void test() { + // First GET request expects an HTML format. The resource grants anything. + CoapClient client = new CoapClient("coap://localhost:"+serverPort+"/"+TARGET); + client.setTimeout(100); + + CoapResponse res1 = client.get(MediaTypeRegistry.TEXT_HTML); + Assert.assertNotNull(res1); + Assert.assertEquals(ResponseCode.CONTENT, res1.getCode()); + Assert.assertEquals(RESPONSE_PAYLOAD, res1.getResponseText()); + + // From now on, the resource only provides the following two formats + testResource.getAcceptSupport().addAcceptable(MediaTypeRegistry.TEXT_CSV); + testResource.getAcceptSupport().addAcceptable(MediaTypeRegistry.APPLICATION_XML); + + // The second GET request expects a format unsupported by the resource + CoapResponse res2 = client.get(MediaTypeRegistry.TEXT_HTML); + Assert.assertNotNull(res2); + Assert.assertEquals(ResponseCode.NOT_ACCEPTABLE, res2.getCode()); + Assert.assertEquals("", res2.getResponseText()); + + // The third GET request expects a supported format + CoapResponse res3 = client.get(MediaTypeRegistry.APPLICATION_XML); + Assert.assertNotNull(res3); + Assert.assertEquals(ResponseCode.CONTENT, res3.getCode()); + Assert.assertEquals(RESPONSE_PAYLOAD, res3.getResponseText()); + } + + private static class TestResource extends CoapResource { + + public TestResource(String name) { + super(name); + } + + @Override + public void handleGET(CoapExchange exchange) { + exchange.respond(RESPONSE_PAYLOAD); + } + } +} diff --git a/californium-core/src/test/java/org/eclipse/californium/core/test/ETagSupportResourceTest.java b/californium-core/src/test/java/org/eclipse/californium/core/test/ETagSupportResourceTest.java new file mode 100644 index 00000000..9170f5dc --- /dev/null +++ b/californium-core/src/test/java/org/eclipse/californium/core/test/ETagSupportResourceTest.java @@ -0,0 +1,120 @@ +package org.eclipse.californium.core.test; + +import java.util.Arrays; + +import org.eclipse.californium.core.CoapClient; +import org.eclipse.californium.core.CoapResponse; +import org.eclipse.californium.core.CoapServer; +import org.eclipse.californium.core.coap.CoAP.ResponseCode; +import org.eclipse.californium.core.network.CoAPEndpoint; +import org.eclipse.californium.core.server.resources.CoapExchange; +import org.eclipse.californium.core.server.resources.ConcurrentCoapResource; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class ETagSupportResourceTest { + + public static final String TARGET = "test"; + + private CoapServer server; + private int serverPort; + + private TestResource testResource; + + @Before + public void startupServer() throws Exception { + System.out.println("\nStart "+getClass().getSimpleName()); + + CoAPEndpoint serverEndpoint = new CoAPEndpoint(); + server = new CoapServer(); + server.addEndpoint(serverEndpoint); + server.add(testResource = new TestResource(TARGET)); + server.start(); + + serverPort = serverEndpoint.getAddress().getPort(); + } + + @After + public void shutdownServer() { + server.destroy(); + System.out.println("End "+getClass().getSimpleName()); + } + + @Test + public void test() { + String currentContent = "AAAA"; + testResource.setContent(currentContent); + + // First GET request expects a 2.05 response with an ETag. + CoapClient client = new CoapClient("coap://localhost:"+serverPort+"/"+TARGET); + client.setTimeout(100); + + CoapResponse res1 = client.get(); + Assert.assertNotNull(res1); + Assert.assertEquals(ResponseCode.CONTENT, res1.getCode()); + Assert.assertEquals(currentContent, res1.getResponseText()); + Assert.assertTrue(res1.getOptions().getETagCount() > 0); + + // Second request validates that the ETag from the first response is still valid. + byte[] etag1 = res1.getOptions().getETags().get(0); + CoapResponse res2 = client.validate(etag1); + Assert.assertNotNull(res2); + Assert.assertEquals(ResponseCode.VALID, res2.getCode()); + Assert.assertTrue(res2.getResponseText().isEmpty()); + Assert.assertTrue(res2.getOptions().getETagCount() > 0); + Assert.assertArrayEquals(etag1, res2.getOptions().getETags().get(0)); + + // The content changes + currentContent = "BBBB"; + testResource.setContent(currentContent); + + // The third request attempts to validate that the ETag from the first response is still valid. + // However, since the content has changed, the server responds a 2.05 with the new content and a new ETag. + CoapResponse res3 = client.validate(etag1); + Assert.assertNotNull(res3); + Assert.assertEquals(ResponseCode.CONTENT, res3.getCode()); + Assert.assertEquals(currentContent, res3.getResponseText()); + Assert.assertTrue(res3.getOptions().getETagCount() > 0); + Assert.assertFalse(Arrays.equals(etag1, res3.getOptions().getETags().get(0))); // has another ETag + + // The forth request validates the ETags from the first and third response. + // The server responds that the third ETag is still valid. + byte[] etag3 = res3.getOptions().getETags().get(0); + CoapResponse res4 = client.validate(etag1, etag3); + Assert.assertNotNull(res4); + Assert.assertEquals(ResponseCode.VALID, res4.getCode()); + Assert.assertTrue(res4.getResponseText().isEmpty()); + Assert.assertTrue(res4.getOptions().getETagCount() > 0); + Assert.assertArrayEquals(etag3, res4.getOptions().getETags().get(0)); + + } + + private static class TestResource extends ConcurrentCoapResource { + + private String content; + + public TestResource(String name) { + super(name, ConcurrentCoapResource.SINGLE_THREADED); + } + + @Override + public void handleGET(CoapExchange exchange) { + exchange.setETag(getETagSupport().getCurrentETag()); + exchange.respond(content); + } + + public void setContent(final String newContent) { + // This Changes the content and update the ETag. This is executed by + // the same thread that also handles requests so that there are no + // race conditions! + execute(new Runnable() { + public void run() { + content = newContent; + getETagSupport().nextETag(); + } + }); + } + } +} diff --git a/californium-core/src/test/java/org/eclipse/californium/core/test/ETagSupportTest.java b/californium-core/src/test/java/org/eclipse/californium/core/test/ETagSupportTest.java new file mode 100644 index 00000000..26c7d1d1 --- /dev/null +++ b/californium-core/src/test/java/org/eclipse/californium/core/test/ETagSupportTest.java @@ -0,0 +1,39 @@ +package org.eclipse.californium.core.test; + +import org.eclipse.californium.core.server.resources.ETagDefaultSupport; +import org.junit.Assert; +import org.junit.Test; + +public class ETagSupportTest { + + @Test + public void test() { + ETagDefaultSupport sup = new ETagDefaultSupport(8, 77); + + // Test 1000 increments + for (int i=0;i<1000;i++) + sup.nextETag(); + + byte[] expected = new byte[] { (byte) (1077 >>> 8), (byte) 1077 }; + Assert.assertArrayEquals(expected, sup.getCurrentETag()); + + // Test higher values and cut at 6 bytes + sup = new ETagDefaultSupport(6, 0xFFFFFFFFFFFFL); + Assert.assertArrayEquals(new byte[] {-1, -1, -1, -1, -1, -1}, sup.getCurrentETag()); + Assert.assertArrayEquals(new byte[0], sup.nextETag()); + + // Test setCurrentETag( X ) + sup.setCurrentETag(new byte[] { 0}); + Assert.assertArrayEquals(new byte[0], sup.getCurrentETag()); + sup.setCurrentETag(new byte[] { 1}); + Assert.assertArrayEquals(new byte[] {1}, sup.getCurrentETag()); + sup.setCurrentETag(new byte[] { 2}); + Assert.assertArrayEquals(new byte[] {2}, sup.getCurrentETag()); + sup.setCurrentETag(new byte[] { 0xF}); + Assert.assertArrayEquals(new byte[] {15}, sup.getCurrentETag()); + sup.setCurrentETag(new byte[] { 1, 0}); + Assert.assertArrayEquals(new byte[] {1, 0}, sup.getCurrentETag()); + Assert.assertArrayEquals(new byte[] {1, 1}, sup.nextETag()); + } + +}