/*******************************************************************************
* Copyright (c) 2014, Institute for Pervasive Computing, ETH Zurich.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the Institute nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
* This file is part of the Californium (Cf) CoAP framework.
******************************************************************************/
package ch.ethz.inf.vs.californium.proxy;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import ch.ethz.inf.vs.californium.coap.CoAP.ResponseCode;
import ch.ethz.inf.vs.californium.coap.MediaTypeRegistry;
import ch.ethz.inf.vs.californium.coap.OptionNumberRegistry;
import ch.ethz.inf.vs.californium.coap.Request;
import ch.ethz.inf.vs.californium.coap.Response;
import ch.ethz.inf.vs.californium.network.config.NetworkConfig;
import ch.ethz.inf.vs.californium.network.config.NetworkConfigDefaults;
import ch.ethz.inf.vs.californium.server.resources.CoapExchange;
import ch.ethz.inf.vs.californium.server.resources.ResourceBase;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.CacheStats;
import com.google.common.cache.LoadingCache;
import com.google.common.primitives.Ints;
/**
* Resource to handle the caching in the proxy.
*
* @author Francesco Corazza
*
*/
public class ProxyCacheResource extends ResourceBase implements CacheResource {
/**
* The time after which an entry is removed. Since it is not possible to set
* the expiration for the single instances, this constant represent the
* upper bound for the cache. The real lifetime will be handled explicitely
* with the max-age option.
*/
private static final int CACHE_RESPONSE_MAX_AGE =
NetworkConfig.getStandard().getInt(NetworkConfigDefaults.HTTP_CACHE_RESPONSE_MAX_AGE);
/**
* Maximum size for the cache.
*/
private static final long CACHE_SIZE =
NetworkConfig.getStandard().getInt(NetworkConfigDefaults.HTTP_CACHE_SIZE);
/**
* The cache. http://code.google.com/p/guava-libraries/wiki/CachesExplained
*/
private final LoadingCache<CacheKey, Response> responseCache;
private boolean enabled = false;
/**
* Instantiates a new proxy cache resource.
*/
public ProxyCacheResource() {
this(false);
}
/**
* Instantiates a new proxy cache resource.
*/
public ProxyCacheResource(boolean enabled) {
super("cache");
this.enabled = enabled;
// builds a new cache that:
// - has a limited size of CACHE_SIZE entries
// - removes entries after CACHE_RESPONSE_MAX_AGE seconds from the last
// write
// - record statistics
responseCache = CacheBuilder.newBuilder().maximumSize(CACHE_SIZE).recordStats().expireAfterWrite(CACHE_RESPONSE_MAX_AGE, TimeUnit.SECONDS).build(new CacheLoader<CacheKey, Response>() {
@Override
public Response load(CacheKey request) throws NullPointerException {
// retreive the response from the incoming request, no
// exceptions are thrown
Response cachedResponse = request.getResponse();
// check for null and raise an exception that clients must
// handle
if (cachedResponse == null) {
throw new NullPointerException();
}
return cachedResponse;
}
});
}
/**
* Puts in cache an entry or, if already present, refreshes it. The method
* first checks the response code, only the 2.xx codes are cached by coap.
* In case of 2.01, 2.02, and 2.04 response codes it invalidates the
* possibly present response. In case of 2.03 it updates the freshness of
* the response with the max-age option provided. In case of 2.05 it creates
* the key and caches the response if the max-age option is higher than
* zero.
*/
@Override
public void cacheResponse(Request request, Response response) {
// enable or disable the caching (debug purposes)
if (!enabled) {
return;
}
// only the response with success codes should be cached
ResponseCode code = response.getCode();
if (ResponseCode.isSuccess(code)) {
// get the request
// Request request = response.getRequest();
CacheKey cacheKey = null;
try {
cacheKey = CacheKey.fromContentTypeOption(request);
} catch (URISyntaxException e) {
LOGGER.warning("Cannot create the cache key: " + e.getMessage());
}
if (code == ResponseCode.CREATED || code == ResponseCode.DELETED || code == ResponseCode.CHANGED) {
// the stored response should be invalidated if the response has
// codes: 2.01, 2.02, 2.04.
invalidateRequest(cacheKey);
} else if (code == ResponseCode.VALID) {
// increase the max-age value according to the new response
// Option maxAgeOption = response.getFirstOption(OptionNumberRegistry.MAX_AGE);
Long maxAgeOption = response.getOptions().getMaxAge();
if (maxAgeOption != null) {
// get the cached response
Response cachedResponse = responseCache.getUnchecked(cacheKey);
// calculate the new parameters
long newCurrentTime = response.getTimestamp();
int newMaxAge = maxAgeOption.intValue();
// set the new parameters
// cachedResponse.getFirstOption(OptionNumberRegistry.MAX_AGE).setIntValue(newMaxAge);
cachedResponse.getOptions().setMaxAge(newMaxAge);
cachedResponse.setTimestamp(newCurrentTime);
LOGGER.finer("Updated cached response");
} else {
LOGGER.warning("No max-age option set in response: " + response);
}
} else if (code == ResponseCode.CONTENT) {
// set max-age if not set
// Option maxAgeOption = response.getFirstOption(OptionNumberRegistry.MAX_AGE);
Long maxAgeOption = response.getOptions().getMaxAge();
if (maxAgeOption == null) {
response.getOptions().setMaxAge(OptionNumberRegistry.DEFAULT_MAX_AGE);
}
if (maxAgeOption > 0) {
// cache the request
try {
// Caches loaded by a CacheLoader will call
// CacheLoader.load(K) to load new values into the cache
// when used the get method.
Response responseInserted = responseCache.get(cacheKey);
if (responseInserted != null) {
// if (Bench_Help.DO_LOG)
LOGGER.finer("Cached response");
} else {
LOGGER.warning("Failed to insert the response in the cache");
}
} catch (Exception e) {
// swallow
LOGGER.log(Level.WARNING, "Exception while inserting the response in the cache", e);
}
} else {
// if the max-age option is set to 0, then the response
// should be invalidated
invalidateRequest(request);
}
} else {
// this code should not be reached
LOGGER.severe("Code not recognized: " + code);
}
}
}
@Override
public CacheStats getCacheStats() {
return responseCache.stats();
}
/**
* Retrieves the response in the cache that matches the request passed, null
* otherwise. The method creates the key for the cache starting from the
* request and checks if the cache contains it. If present, the method
* updates the max-age of the linked response to consider the time passed in
* the cache (according to the freshness model) and returns it. On the
* contrary, if the response has passed its expiration time, it is
* invalidated and the method returns null.
*/
@Override
public Response getResponse(Request request) {
if (!enabled) {
return null;
}
// search the desired representation
Response response = null;
CacheKey cacheKey = null;
try {
for (CacheKey acceptKey : CacheKey.fromAcceptOptions(request)) {
response = responseCache.getIfPresent(acceptKey);
cacheKey = acceptKey;
if (response != null) {
break;
}
}
} catch (URISyntaxException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// if the response is not null, manage the cached response
if (response != null) {
LOGGER.finer("Cache hit");
// check if the response is expired
long currentTime = System.nanoTime();
int nanosLeft = getRemainingLifetime(response, currentTime);
if (nanosLeft > 0) {
// if the response can be used, then update its max-age to
// consider the aging of the response while in the cache
response.getOptions().setMaxAge(nanosLeft);
// set the current time as the response timestamp
response.setTimestamp(currentTime);
} else {
LOGGER.finer("Expired response");
// try to validate the response
response = validate(cacheKey);
if (response != null) {
LOGGER.finer("Validation successful");
} else {
invalidateRequest(cacheKey);
}
}
}
return response;
}
/*
* (non-Javadoc)
* @see ch.ethz.inf.vs.californium.endpoint.resources.CacheResource#
* invalidateResponse(ch.ethz.inf.vs.californium.coap.Response)
*/
@Override
public void invalidateRequest(Request request) {
try {
invalidateRequest(CacheKey.fromAcceptOptions(request));
} catch (URISyntaxException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
LOGGER.finer("Invalidated request");
}
@Override
public void handleDELETE(CoapExchange exchange) {
responseCache.invalidateAll();
exchange.respond(ResponseCode.DELETED);
}
@Override
public void handleGET(CoapExchange exchange) {
StringBuilder builder = new StringBuilder();
builder.append("Available commands:\n - GET: show cached values\n - DELETE: empty the cache\n - POST: enable/disable caching\n");
// get cache values
builder.append("\nCached values:\n");
for (CacheKey cachedRequest : responseCache.asMap().keySet()) {
Response response = responseCache.asMap().get(cachedRequest);
builder.append(cachedRequest.getProxyUri().toString() + " (" +
MediaTypeRegistry.toString(cachedRequest.getMediaType()) + ") > " + getRemainingLifetime(response) + " seconds | (" + cachedRequest.getMediaType() + ")\n");
}
exchange.respond(ResponseCode.CONTENT, builder.toString());
}
@Override
public void handlePOST(CoapExchange exchange) {
enabled = !enabled;
String content = enabled ? "Enabled" : "Disabled";
exchange.respond(ResponseCode.CHANGED, content);
}
private int getRemainingLifetime(Response response) {
return getRemainingLifetime(response, System.nanoTime());
}
/**
* Method that checks if the lifetime allowed for the response if expired.
* The result is calculated with the initial timestamp (when the response
* has been received) and the max-age option compared against the current
* timestamp. If the max-age option is not specified, it will be assumed the
* default (60 seconds).
*
* @param response
* the response
* @param currentTime
* @return true, if is expired
*/
private int getRemainingLifetime(Response response, long currentTime) {
// get the timestamp
long arriveTime = response.getTimestamp();
// Option maxAgeOption = response.getFirstOption(OptionNumberRegistry.MAX_AGE);
Long maxAgeOption = response.getOptions().getMaxAge();
int oldMaxAge = OptionNumberRegistry.DEFAULT_MAX_AGE;
if (maxAgeOption != null) {
oldMaxAge = maxAgeOption.intValue();
}
// calculate the time that the response has spent in the cache
double secondsInCache = TimeUnit.NANOSECONDS.toSeconds(currentTime - arriveTime);
int cacheTime = Ints.checkedCast(Math.round(secondsInCache));
return oldMaxAge - cacheTime;
}
private void invalidateRequest(CacheKey cacheKey) {
responseCache.invalidate(cacheKey);
}
private void invalidateRequest(List<CacheKey> cacheKeys) {
responseCache.invalidateAll(cacheKeys);
}
private Response validate(CacheKey cachedRequest) {
// TODO
return null;
}
/**
* Nested class that normalizes the variable fields of the coap requests to
* be used as a key for the cache. The class tries to handle also the
* different requests that must refer to the same response (e.g., requests
* that with or without the accept options produce the same response).
*
* @author Francesco Corazza
*/
private static final class CacheKey {
private final String proxyUri;
private final int mediaType;
private Response response;
private final byte[] payload;
/**
* Creates a list of keys for the cache from a request with multiple
* accept options set. Method needed to search for content-type
* wildcards in the cache (text/* means: text/plain, text/html,
* text/xml, text/csv, etc.). If the accept option is not set, it simply
* gives back the keys for every representation.
*
* @param request
* @return
* @throws URISyntaxException
*/
private static List<CacheKey> fromAcceptOptions(Request request) throws URISyntaxException {
if (request == null) {
throw new IllegalArgumentException("request == null");
}
List<CacheKey> cacheKeys = new LinkedList<ProxyCacheResource.CacheKey>();
String proxyUri = request.getOptions().getProxyURI();
try {
proxyUri = URLEncoder.encode(proxyUri, "ISO-8859-1");
} catch (UnsupportedEncodingException e) {
LOGGER.warning("ISO-8859-1 do not support this encoding: " + e.getMessage());
throw new URISyntaxException("ISO-8859-1 do not support this encoding", e.getMessage());
}
byte[] payload = request.getPayload();
// Implementation in new Cf (Only one accept option allowed)
Integer accept = request.getOptions().getAccept();
if (accept != null) {
int mediaType = accept.intValue();
CacheKey cacheKey = new CacheKey(proxyUri, mediaType, payload);
cacheKeys.add(cacheKey);
} else {
// if the accept options are not set, simply set all media types
// FIXME not efficient
for (Integer acceptType : MediaTypeRegistry.getAllMediaTypes()) {
CacheKey cacheKey = new CacheKey(proxyUri, acceptType, payload);
cacheKeys.add(cacheKey);
}
}
return cacheKeys;
}
/**
* Create a key for the cache starting from a request and the
* content-type of the corresponding response.
*
* @param request
* @return
* @throws URISyntaxException
*/
private static CacheKey fromContentTypeOption(Request request) throws URISyntaxException {
if (request == null) {
throw new IllegalArgumentException("request == null");
}
Response response = request.getResponse();
if (response == null) {
return fromAcceptOptions(request).get(0);
}
String proxyUri = request.getOptions().getProxyURI();
Integer mediaType = response.getOptions().getContentFormat();
if (mediaType == null)
mediaType = MediaTypeRegistry.TEXT_PLAIN;
byte[] payload = request.getPayload();
// create the new cacheKey
CacheKey cacheKey = new CacheKey(proxyUri, mediaType, payload);
cacheKey.setResponse(response);
return cacheKey;
}
public CacheKey(String proxyUri, int mediaType, byte[] payload) {
this.proxyUri = proxyUri;
this.mediaType = mediaType;
this.payload = payload;
}
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
CacheKey other = (CacheKey) obj;
if (mediaType != other.mediaType) {
return false;
}
if (!Arrays.equals(payload, other.payload)) {
return false;
}
if (proxyUri == null) {
if (other.proxyUri != null) {
return false;
}
} else if (!proxyUri.equals(other.proxyUri)) {
return false;
}
return true;
}
/**
* @return the mediaType
*/
public int getMediaType() {
return mediaType;
}
/**
* @return the proxyUri
*/
public String getProxyUri() {
return proxyUri;
}
/**
* @return the response
*/
public Response getResponse() {
return response;
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + mediaType;
result = prime * result + Arrays.hashCode(payload);
result = prime * result + (proxyUri == null ? 0 : proxyUri.hashCode());
return result;
}
private void setResponse(Response response) {
this.response = response;
}
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}