/*
* Copyright Ericsson AB 2011-2014. All Rights Reserved.
*
* The contents of this file are subject to the Lesser GNU Public License,
* (the "License"), either version 2.1 of the License, or
* (at your option) any later version.; you may not use this file except in
* compliance with the License. You should have received a copy of the
* License along with this software. If not, it can be
* retrieved online at https://www.gnu.org/licenses/lgpl.html. Moreover
* it could also be requested from Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
* WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
* EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
* OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND,
* EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
* LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE,
* YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
*
* IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
* WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
* REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR
* DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL
* DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY
* (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
* INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE
* OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH
* HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
*
*/
package com.ericsson.deviceaccess.coap.basedriver.osgi;
import com.ericsson.common.util.BitUtil;
import com.ericsson.common.util.function.FunctionalUtil;
import com.ericsson.deviceaccess.coap.basedriver.api.CoAPException;
import com.ericsson.deviceaccess.coap.basedriver.api.message.CoAPMessage.CoAPMessageType;
import com.ericsson.deviceaccess.coap.basedriver.api.message.CoAPOptionHeader;
import com.ericsson.deviceaccess.coap.basedriver.api.message.CoAPOptionName;
import com.ericsson.deviceaccess.coap.basedriver.api.message.CoAPRequest;
import com.ericsson.deviceaccess.coap.basedriver.api.message.CoAPRequestCode;
import com.ericsson.deviceaccess.coap.basedriver.api.message.CoAPResponse;
import com.ericsson.deviceaccess.coap.basedriver.api.resources.CoAPObservationResource;
import com.ericsson.deviceaccess.coap.basedriver.api.resources.CoAPResource;
import com.ericsson.deviceaccess.coap.basedriver.api.resources.CoAPResourceObserver;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class is responsible for handling the observation relationships. It
* caches the received responses.
* <p/>
* Note that in case there will be several clients wanting to follow the same
* CoAP resource, the observation handler will first check the cache. If there
* doesn't exist a fresh response, a further observe request is sent. This means
* that all listeners will be called when a response for that "refresh" request
* is received.
*/
public class ObservationHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(ObservationHandler.class);
private final Map<URI, CoAPObservationResource> observedResources;
private final Map<URI, CoAPRequest> originalRequests;
private final Map<URI, RefreshTask> cachedResponses;
private final Timer timer;
private final LocalCoAPEndpoint endpoint;
/**
* Constructor
*
* @param endpoint the local endpoint
*/
public ObservationHandler(LocalCoAPEndpoint endpoint) {
this.observedResources = new HashMap<>();
this.cachedResponses = new HashMap<>();
this.originalRequests = new HashMap<>();
this.endpoint = endpoint;
this.timer = new Timer();
}
/**
* This method is called by the local endpoint when a response with observe
* option is received.
*
* @param originalRequest original request that the response is related to
* @param resp response from the CoAP Server
* @throws CoAPException
*/
public void handleObserveResponse(CoAPRequest originalRequest, CoAPResponse resp) throws CoAPException {
// Handle responses related to observe relationships
LOGGER.debug("Response is related to an observation relationship");
URI uri = originalRequest.getUriFromRequest();
CoAPObservationResource res = observedResources.get(uri);
if (res == null) {
return;
}
if (resp.getOptionHeaders(CoAPOptionName.OBSERVE).isEmpty()) {
// this means the response is terminating an observation relationship
observedResources.remove(uri);
res.getObservers()
.forEach(obs -> {
obs.observationRelationshipTerminated(resp, res, originalRequests.get(uri));
});
FunctionalUtil.putAndClean(cachedResponses, uri, null, v -> v.cancel());
return;
}
byte[] bytes = resp.getOptionHeaders(CoAPOptionName.OBSERVE).get(0).getValue();
if (bytes.length == 2) {
short test = BitUtil.mergeBytesToShort(bytes[0], bytes[1]);
int observeValue = test & 0xFFFF;
LOGGER.debug("Masked observe value in observation handler [" + observeValue + "]");
if (resp.getOptionHeaders(CoAPOptionName.BLOCK2).isEmpty()) {
// Check if the notification is fresh
if (!res.isFresh(observeValue, new java.util.Date())) {
//if the response is not fresh, it can be discarded!
return;
}
} else {
System.out.println("TODO handling of freshness of blockwise observe responses");
}
}
// Put in the cached responses, replacing the old task if
RefreshTask task = new RefreshTask(resp, uri);
FunctionalUtil.putAndClean(cachedResponses, uri, task, v -> v.cancel());
// Read the max-age option
timer.schedule(task, resp.getMaxAge() * 1000);
// TODO populate resource with more data?
res.setContent(resp.getPayload());
CoAPRequest req = originalRequests.get(res.getUri());
res.getObservers()
.forEach(observer -> {
observer.observeResponseReceived(resp, res, req);
});
}
/**
* This method terminates an observation relationship between the given
* resource and observer instance
*
* @param resource resource to which the observation is related to
* @param observer observer of the resource
* @return
* @throws CoAPException
*/
public boolean terminateObservationRelationship(CoAPResource resource,
CoAPResourceObserver observer) throws CoAPException {
boolean removed = resource.removeObserver(observer);
// If no more observers are left, finish the observation
// relationship by sending a request without observe option
if (removed && resource.getObservers().isEmpty()) {
// TODO should the termination request be confirmable or non-confirmable
InetSocketAddress sockaddr = null;
try {
String socketAddress = resource.getUri().getHost();
InetAddress address = InetAddress.getByName(socketAddress);
sockaddr = new InetSocketAddress(address, resource.getUri().getPort());
} catch (UnknownHostException e) {
throw new CoAPException(e);
}
CoAPRequest req = endpoint.createCoAPRequest(
CoAPMessageType.CONFIRMABLE,
CoAPRequestCode.GET,
sockaddr,
resource.getUri(),
null);
endpoint.sendRequest(req);
originalRequests.remove(resource.getUri());
// TODO identify if the relationship was terminated!
observedResources.remove(resource.getUri());
}
return removed;
}
public boolean isObserved(URI uri) {
return observedResources.get(uri) != null;
}
public CoAPResource getResource(URI uri) {
return observedResources.get(uri);
}
/**
* Remove cached response (expired)
*
* @param uri URI to the resource
*/
protected synchronized void removeCachedResponse(URI uri) {
LOGGER.debug("Cached response for URI [" + uri.toString() + " expired, remove from cache");
cachedResponses.remove(uri);
}
/**
* This methods create an observation relationship with the given uri and
* the observer instance
*
* @param uri URI to the resource
* @param observer observer who will be notified about the changes
* @return CoAPResource representing the given URI
* @throws CoAPException
*/
public CoAPResource createObservationRelationship(URI uri,
CoAPResourceObserver observer) throws CoAPException {
LOGGER.debug("Create observation relationship to URI [" + uri + "]");
CoAPObservationResource resource;
// Check if there already exist observation for this resource
if (observedResources.containsKey(uri)) {
// get the resource based on key
resource = observedResources.get(uri);
resource.addObserver(observer);
// Notify with a cached response
if (cachedResponses.containsKey(uri)) {
LOGGER.debug("A fresh response still found in cache");
observer.observeResponseReceived(
cachedResponses.get(uri).getResponse(),
resource,
originalRequests.get(uri));
} // If the response in the cache is older than max-age + max-ofe,
// send a further observation request
else {
CoAPRequest req = createObservationRequest(uri);
// Store in the local memory the original request
originalRequests.put(uri, req);
endpoint.sendRequest(req);
}
} else {
// Otherwise create a new observation request and add it in the list of observed resources
CoAPRequest req = createObservationRequest(uri);
resource = new CoAPObservationResource(uri);
resource.addObserver(observer);
originalRequests.put(uri, req);
observedResources.put(uri, resource);
endpoint.sendRequest(req);
}
return resource;
}
/**
* Create observation request based on the given URI. This request contains
* the observe option.
*
* @param uri URI
* @return
* @throws CoAPException
*/
private CoAPRequest createObservationRequest(URI uri) throws CoAPException {
InetSocketAddress sockaddr = null;
try {
String socketAddress = uri.getHost();
InetAddress address = InetAddress.getByName(socketAddress);
sockaddr = new InetSocketAddress(address, uri.getPort());
} catch (UnknownHostException e) {
throw new CoAPException(e);
}
// Add observe option in the request
CoAPRequest req = endpoint.createCoAPRequest(
CoAPMessageType.CONFIRMABLE,
CoAPRequestCode.GET,
sockaddr,
uri,
null);
// A non-negative integer which is represented in network byte order
short observe = 0;
byte[] observeBytes = BitUtil.splitShortToBytes(observe);
CoAPOptionHeader observeOpt = new CoAPOptionHeader(CoAPOptionName.OBSERVE, observeBytes);
req.addOptionHeader(observeOpt);
req.generateTokenHeader();
return req;
}
/**
* Cancel the timer and its tasks. This method is needed when stopping the
* bundle.
*/
public void stopService() {
timer.cancel();
}
/**
* Inner class to handle timers for cached observe responses
*/
private class RefreshTask extends TimerTask {
private final CoAPResponse cachedResponse;
private final URI uri;
/**
* Constructor
*
* @param cachedResponse
* @param uri
*/
protected RefreshTask(CoAPResponse cachedResponse, URI uri) {
this.cachedResponse = cachedResponse;
this.uri = uri;
}
@Override
public void run() {
LOGGER.debug("Cached response expired");
// If the cached response expires, remove first the cached stuff
removeCachedResponse(uri);
// TODO if cached response expires, should send a new GET request!!
try {
LOGGER.debug("Send a new GET request towards the server to refresh the observation");
CoAPRequest req = createObservationRequest(uri);
// Do no update the hashmap, keep the original request there
endpoint.sendRequest(req);
} catch (CoAPException e) {
LOGGER.warn("Sending new GET request failed.", e);
}
}
public CoAPResponse getResponse() {
return this.cachedResponse;
}
}
}