Skip to content

Commit

Permalink
Dealt with issue eclipse-archived#5 (ETag convenience). Added an inte…
Browse files Browse the repository at this point in the history
…rface ETagSupport

and a default implementation ETagDefaultSupport and JUnit tests. A
CoapResource now has a ETagSupport object that generates ETags if
required. If an incoming request has ETags, the resource automatically
validates them against the current value in the ETagSupport object and
responds with a 2.03 (Valid) if an ETag matches.
  • Loading branch information
martinlanter committed Aug 3, 2014
1 parent da7110d commit 7c334f8
Show file tree
Hide file tree
Showing 5 changed files with 372 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -41,6 +43,8 @@
import org.eclipse.californium.core.observe.ObserveRelationContainer;
import org.eclipse.californium.core.server.ServerMessageDeliverer;
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;
Expand Down Expand Up @@ -159,6 +163,9 @@ public class CoapResource implements Resource {
/* The notification orderer. */
private ObserveNotificationOrderer notificationOrderer;

/* Support for ETags */
private ETagSupport etagSupport;

/**
* Constructs a new resource with the specified name.
*
Expand All @@ -184,8 +191,8 @@ public CoapResource(String name, boolean visible) {
this.observers = new CopyOnWriteArrayList<ResourceObserver>();
this.observeRelations = new ObserveRelationContainer();
this.notificationOrderer = new ObserveNotificationOrderer();
this.etagSupport = createETagSupport();
}


/**
* Handles any request in the given exchange. By default it responds
Expand All @@ -201,6 +208,9 @@ 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;

switch (code) {
case GET: handleGET(new CoapExchange(exchange, this)); break;
case POST: handlePOST(new CoapExchange(exchange, this)); break;
Expand All @@ -209,6 +219,32 @@ 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, 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;
}

/**
* Handles the GET request in the given CoAPExchange. By default it
* responds with a 4.05 (Method Not Allowed). Override this method to
Expand Down Expand Up @@ -718,6 +754,24 @@ protected void notifyObserverRelations() {
relation.notifyObservers();
}
}

/**
* Creates a new ETagSupport object.
*
* @return an ETagSupport object
*/
protected ETagSupport createETagSupport() {
return new ETagDefaultSupport();
}

/**
* Gets the ETagSupport object or null if none is present.
*
* @return the ETagSupport object
*/
public ETagSupport getETagSupport() {
return etagSupport;
}

/* (non-Javadoc)
* @see org.eclipse.californium.core.server.resources.Resource#getChildren()
Expand Down
Original file line number Diff line number Diff line change
@@ -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<etag.length && i<bytes;i++)
cur |= (etag[i] << (etag.length - i - 1)*8);
this.current.set(cur);
}

/* (non-Javadoc)
* @see org.eclipse.californium.core.server.resources.ETagSupport#getCurrentETagAsString()
*/
public String getCurrentETagAsString() {
byte[] etag = getCurrentETag();
StringBuffer string = new StringBuffer("");
for(byte b:etag) string.append(String.format("%02x", b&0xff));
return string.toString();
}

/**
* Converts the specified long value to a byte array of maximum maxSize
* length. Cuts off leading zeros.
*
* @param value the value
* @param maxSize the max size of the byte array
* @return the byte[] the byte array
*/
private byte[] long2bytes(long value, int maxSize) {
int length = maxSize;
// reduce length so that there will be no leading zeros
for (;( (value >>> (length-1)*8) & 0xFF) == 0 && length > 0;length--);
byte[] etag = new byte[length];
for (int i=0;i<length;i++) {
etag[length - i - 1] = (byte) (value >>> i*8);
}
return etag;
}

/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
public String toString() {
return "DefaultETagSupport(current: 0x"+getCurrentETagAsString()+", bytes: "+bytes+")";
}

}
Original file line number Diff line number Diff line change
@@ -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();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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);
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();
}
});
}
}
}
Loading

0 comments on commit 7c334f8

Please sign in to comment.