/*******************************************************************************
* Copyright (c) 2016 Sierra Wireless and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
* and the Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.html.
*
* Contributors:
* Sierra Wireless - initial API and implementation
*******************************************************************************/
package org.eclipse.leshan.server.californium.impl;
import static org.eclipse.leshan.server.californium.impl.CoapRequestBuilder.CTX_REGID;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.Request;
import org.eclipse.californium.core.coap.Response;
import org.eclipse.californium.core.network.Endpoint;
import org.eclipse.californium.core.observe.NotificationListener;
import org.eclipse.californium.core.observe.ObservationStore;
import org.eclipse.leshan.ResponseCode;
import org.eclipse.leshan.core.model.LwM2mModel;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
import org.eclipse.leshan.core.node.codec.CodecException;
import org.eclipse.leshan.core.node.codec.LwM2mNodeDecoder;
import org.eclipse.leshan.core.observation.Observation;
import org.eclipse.leshan.core.request.ContentFormat;
import org.eclipse.leshan.core.request.exception.InvalidResponseException;
import org.eclipse.leshan.core.response.ObserveResponse;
import org.eclipse.leshan.server.californium.CaliforniumRegistrationStore;
import org.eclipse.leshan.server.model.LwM2mModelProvider;
import org.eclipse.leshan.server.observation.ObservationListener;
import org.eclipse.leshan.server.observation.ObservationService;
import org.eclipse.leshan.server.registration.Registration;
import org.eclipse.leshan.util.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implementation of the {@link ObservationService} accessing the persisted observation via the provided
* {@link CaliforniumRegistrationStore}.
*
* When a new observation is added or changed or canceled, the registered listeners are notified.
*/
public class ObservationServiceImpl implements ObservationService, NotificationListener {
private final Logger LOG = LoggerFactory.getLogger(ObservationServiceImpl.class);
private final CaliforniumRegistrationStore registrationStore;
private final LwM2mModelProvider modelProvider;
private final LwM2mNodeDecoder decoder;
private Endpoint secureEndpoint;
private Endpoint nonSecureEndpoint;
private final List<ObservationListener> listeners = new CopyOnWriteArrayList<>();
/**
* Creates an instance of {@link ObservationServiceImpl}
*
* @param store instance of californium's {@link ObservationStore}
* @param modelProvider instance of {@link LwM2mModelProvider}
* @param decoder instance of {@link LwM2mNodeDecoder}
*/
public ObservationServiceImpl(CaliforniumRegistrationStore store, LwM2mModelProvider modelProvider,
LwM2mNodeDecoder decoder) {
this.registrationStore = store;
this.modelProvider = modelProvider;
this.decoder = decoder;
}
public void addObservation(Registration registration, Observation observation) {
for (Observation existing : registrationStore.addObservation(registration.getId(), observation)) {
cancel(existing);
}
for (ObservationListener listener : listeners) {
listener.newObservation(observation, registration);
}
}
public void setNonSecureEndpoint(Endpoint endpoint) {
nonSecureEndpoint = endpoint;
}
public void setSecureEndpoint(Endpoint endpoint) {
secureEndpoint = endpoint;
}
@Override
public int cancelObservations(Registration registration) {
// check registration id
String registrationId = registration.getId();
if (registrationId == null)
return 0;
Collection<Observation> observations = registrationStore.removeObservations(registrationId);
if (observations == null)
return 0;
for (Observation observation : observations) {
cancel(observation);
}
return observations.size();
}
@Override
public int cancelObservations(Registration registration, String resourcepath) {
if (registration == null || registration.getId() == null || resourcepath == null || resourcepath.isEmpty())
return 0;
Set<Observation> observations = getObservations(registration.getId(), resourcepath);
for (Observation observation : observations) {
cancelObservation(observation);
}
return observations.size();
}
@Override
public void cancelObservation(Observation observation) {
if (observation == null)
return;
registrationStore.removeObservation(observation.getRegistrationId(), observation.getId());
cancel(observation);
}
private void cancel(Observation observation) {
if (secureEndpoint != null)
secureEndpoint.cancelObservation(observation.getId());
if (nonSecureEndpoint != null)
nonSecureEndpoint.cancelObservation(observation.getId());
for (ObservationListener listener : listeners) {
listener.cancelled(observation);
}
}
@Override
public Set<Observation> getObservations(Registration registration) {
return getObservations(registration.getId());
}
private Set<Observation> getObservations(String registrationId) {
if (registrationId == null)
return Collections.emptySet();
return new HashSet<>(registrationStore.getObservations(registrationId));
}
private Set<Observation> getObservations(String registrationId, String resourcePath) {
if (registrationId == null || resourcePath == null)
return Collections.emptySet();
Set<Observation> result = new HashSet<>();
LwM2mPath lwPath = new LwM2mPath(resourcePath);
for (Observation obs : getObservations(registrationId)) {
if (lwPath.equals(obs.getPath())) {
result.add(obs);
}
}
return result;
}
/**
* @return the Californium {@link ObservationStore}
*/
public ObservationStore getObservationStore() {
return registrationStore;
}
@Override
public void addListener(ObservationListener listener) {
listeners.add(listener);
}
@Override
public void removeListener(ObservationListener listener) {
listeners.remove(listener);
}
// ********** NotificationListener interface **********//
@Override
public void onNotification(Request coapRequest, Response coapResponse) {
LOG.trace("notification received for request {}: {}", coapRequest, coapResponse);
if (listeners.isEmpty())
return;
// get registration Id
String regid = coapRequest.getUserContext().get(CTX_REGID);
// get observation for this request
Observation observation = registrationStore.getObservation(regid, coapResponse.getToken());
if (observation == null) {
LOG.error("Unexpected error: Unable to find observation with token {} for registration {}", observation,
coapResponse.getToken());
return;
}
// get registration
Registration registration = registrationStore.getRegistration(observation.getRegistrationId());
if (registration == null) {
LOG.error("Unexpected error: There is no registration with id {} for this observation {}",
observation.getRegistrationId(), observation);
return;
}
try {
// get model for this registration
LwM2mModel model = modelProvider.getObjectModel(registration);
// create response
ObserveResponse response = createObserveResponse(observation, model, coapResponse);
// notify all listeners
for (ObservationListener listener : listeners) {
listener.onResponse(observation, registration, response);
}
} catch (InvalidResponseException e) {
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("Invalid notification for observation [%s]", observation), e);
}
for (ObservationListener listener : listeners) {
listener.onError(observation, registration, e);
}
} catch (RuntimeException e) {
if (LOG.isErrorEnabled()) {
LOG.error(String.format("Unable to handle notification for observation [%s]", observation), e);
}
for (ObservationListener listener : listeners) {
listener.onError(observation, registration, e);
}
}
}
private ObserveResponse createObserveResponse(Observation observation, LwM2mModel model, Response coapResponse) {
// // We handle CONTENT and CHANGED response only
if (coapResponse.getCode() != CoAP.ResponseCode.CHANGED
&& coapResponse.getCode() != CoAP.ResponseCode.CONTENT) {
throw new InvalidResponseException("Unexpected response code [%s] for %s", coapResponse.getCode(),
observation);
}
// get content format
ContentFormat contentFormat = null;
if (coapResponse.getOptions().hasContentFormat()) {
contentFormat = ContentFormat.fromCode(coapResponse.getOptions().getContentFormat());
}
// decode response
try {
List<TimestampedLwM2mNode> timestampedNodes = decoder.decodeTimestampedData(coapResponse.getPayload(),
contentFormat, observation.getPath(), model);
// create lwm2m response
if (timestampedNodes.size() == 1 && !timestampedNodes.get(0).isTimestamped()) {
return new ObserveResponse(ResponseCode.CONTENT, timestampedNodes.get(0).getNode(), null, observation,
null, coapResponse);
} else {
return new ObserveResponse(ResponseCode.CONTENT, null, timestampedNodes, observation, null,
coapResponse);
}
} catch (CodecException e) {
if (LOG.isDebugEnabled()) {
byte[] payload = coapResponse.getPayload() == null ? new byte[0] : coapResponse.getPayload();
LOG.debug(String.format("Unable to decode notification payload [%s] of observation [%s] ",
Hex.encodeHexString(payload), observation), e);
}
throw new InvalidResponseException(e, "Unable to decode notification payload of observation [%s] ",
observation);
}
}
}