/*******************************************************************************
* 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
* Achim Kraus (Bosch Software Innovations GmbH) - replace serialize/parse in
* unsafeGetObservation() with
* ObservationUtil.shallowClone.
* Reuse already created Key in
* setContext().
*******************************************************************************/
package org.eclipse.leshan.server.californium.impl;
import static org.eclipse.leshan.server.californium.impl.CoapRequestBuilder.*;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.eclipse.californium.core.observe.ObservationUtil;
import org.eclipse.californium.elements.CorrelationContext;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.observation.Observation;
import org.eclipse.leshan.server.Startable;
import org.eclipse.leshan.server.Stoppable;
import org.eclipse.leshan.server.californium.CaliforniumRegistrationStore;
import org.eclipse.leshan.server.registration.Deregistration;
import org.eclipse.leshan.server.registration.ExpirationListener;
import org.eclipse.leshan.server.registration.Registration;
import org.eclipse.leshan.server.registration.RegistrationUpdate;
import org.eclipse.leshan.server.registration.UpdatedRegistration;
import org.eclipse.leshan.util.Key;
import org.eclipse.leshan.util.NamedThreadFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An in memory store for registration and observation.
*/
public class InMemoryRegistrationStore implements CaliforniumRegistrationStore, Startable, Stoppable {
private final Logger LOG = LoggerFactory.getLogger(InMemoryRegistrationStore.class);
// Data structure
private final Map<String /* end-point */, Registration> regsByEp = new HashMap<>();
private Map<Key, org.eclipse.californium.core.observe.Observation> obsByToken = new HashMap<>();
private Map<String, List<Key>> tokensByRegId = new HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
// Listener use to notify when a registration expires
private ExpirationListener expirationListener;
private final ScheduledExecutorService schedExecutor;
private final long cleanPeriod; // in seconds
public InMemoryRegistrationStore() {
this(2); // default clean period : 2s
}
public InMemoryRegistrationStore(long cleanPeriodInSec) {
this(Executors.newScheduledThreadPool(1,
new NamedThreadFactory(String.format("InMemoryRegistrationStore Cleaner (%ds)", cleanPeriodInSec))),
cleanPeriodInSec);
}
public InMemoryRegistrationStore(ScheduledExecutorService schedExecutor, long cleanPeriodInSec) {
this.schedExecutor = schedExecutor;
this.cleanPeriod = cleanPeriodInSec;
}
/* *************** Leshan Registration API **************** */
@Override
public Deregistration addRegistration(Registration registration) {
try {
lock.writeLock().lock();
Registration registrationRemoved = regsByEp.put(registration.getEndpoint(), registration);
if (registrationRemoved != null) {
Collection<Observation> observationsRemoved = unsafeRemoveAllObservations(registrationRemoved.getId());
return new Deregistration(registrationRemoved, observationsRemoved);
}
} finally {
lock.writeLock().unlock();
}
return null;
}
@Override
public UpdatedRegistration updateRegistration(RegistrationUpdate update) {
try {
lock.writeLock().lock();
Registration registration = getRegistration(update.getRegistrationId());
if (registration == null) {
return null;
} else {
Registration updatedRegistration = update.update(registration);
regsByEp.put(updatedRegistration.getEndpoint(), updatedRegistration);
return new UpdatedRegistration(registration, updatedRegistration);
}
} finally {
lock.writeLock().unlock();
}
}
@Override
public Registration getRegistration(String registrationId) {
try {
lock.readLock().lock();
if (registrationId != null) {
for (Registration registration : regsByEp.values()) {
if (registrationId.equals(registration.getId())) {
return registration;
}
}
}
return null;
} finally {
lock.readLock().unlock();
}
}
@Override
public Registration getRegistrationByEndpoint(String endpoint) {
try {
lock.readLock().lock();
return regsByEp.get(endpoint);
} finally {
lock.readLock().unlock();
}
}
@Override
public Registration getRegistrationByAdress(InetSocketAddress address) {
// TODO we should create an index instead of iterate all over the collection
for (Registration r : regsByEp.values()) {
if (address.getPort() == r.getPort() && address.getAddress().equals(r.getAddress())) {
return r;
}
}
return null;
}
@Override
public Iterator<Registration> getAllRegistrations() {
try {
lock.readLock().lock();
return new ArrayList<>(regsByEp.values()).iterator();
} finally {
lock.readLock().unlock();
}
}
@Override
public Deregistration removeRegistration(String registrationId) {
try {
lock.writeLock().lock();
Registration registration = getRegistration(registrationId);
if (registration != null) {
Collection<Observation> observationsRemoved = unsafeRemoveAllObservations(registration.getId());
regsByEp.remove(registration.getEndpoint());
return new Deregistration(registration, observationsRemoved);
}
return null;
} finally {
lock.writeLock().unlock();
}
}
/* *************** Leshan Observation API **************** */
/*
* The observation is not persisted here, it is done by the Californium layer (in the implementation of the
* org.eclipse.californium.core.observe.ObservationStore#add method)
*/
@Override
public Collection<Observation> addObservation(String registrationId, Observation observation) {
List<Observation> removed = new ArrayList<>();
try {
lock.writeLock().lock();
// cancel existing observations for the same path and registration id.
for (Observation obs : unsafeGetObservations(registrationId)) {
if (observation.getPath().equals(obs.getPath()) && !Arrays.equals(observation.getId(), obs.getId())) {
unsafeRemoveObservation(obs.getId());
removed.add(obs);
}
}
} finally {
lock.writeLock().unlock();
}
return removed;
}
@Override
public Observation removeObservation(String registrationId, byte[] observationId) {
try {
lock.writeLock().lock();
Observation observation = build(unsafeGetObservation(new Key(observationId)));
if (observation != null && registrationId.equals(observation.getRegistrationId())) {
unsafeRemoveObservation(observationId);
return observation;
}
return null;
} finally {
lock.writeLock().unlock();
}
}
@Override
public Observation getObservation(String registrationId, byte[] observationId) {
try {
lock.readLock().lock();
Observation observation = build(unsafeGetObservation(new Key(observationId)));
if (observation != null && registrationId.equals(observation.getRegistrationId())) {
return observation;
}
return null;
} finally {
lock.readLock().unlock();
}
}
@Override
public Collection<Observation> getObservations(String registrationId) {
try {
lock.readLock().lock();
return unsafeGetObservations(registrationId);
} finally {
lock.readLock().unlock();
}
}
@Override
public Collection<Observation> removeObservations(String registrationId) {
try {
lock.writeLock().lock();
return unsafeRemoveAllObservations(registrationId);
} finally {
lock.writeLock().unlock();
}
}
/* *************** Californium ObservationStore API **************** */
@Override
public void add(org.eclipse.californium.core.observe.Observation obs) {
if (obs != null) {
try {
lock.writeLock().lock();
validateObservation(obs);
String registrationId = extractRegistrationId(obs);
Key token = new Key(obs.getRequest().getToken());
org.eclipse.californium.core.observe.Observation previousObservation = obsByToken.put(token, obs);
if (!tokensByRegId.containsKey(registrationId)) {
tokensByRegId.put(registrationId, new ArrayList<Key>());
}
tokensByRegId.get(registrationId).add(token);
// log any collisions
if (previousObservation != null) {
LOG.warn(
"Token collision ? observation from request [{}] will be replaced by observation from request [{}] ",
previousObservation.getRequest(), obs.getRequest());
}
} finally {
lock.writeLock().unlock();
}
}
}
@Override
public org.eclipse.californium.core.observe.Observation get(byte[] token) {
try {
lock.readLock().lock();
return unsafeGetObservation(new Key(token));
} finally {
lock.readLock().unlock();
}
}
@Override
public void setContext(byte[] token, CorrelationContext ctx) {
try {
lock.writeLock().lock();
Key key = new Key(token);
org.eclipse.californium.core.observe.Observation obs = obsByToken.get(key);
if (obs != null) {
obsByToken.put(key,
new org.eclipse.californium.core.observe.Observation(obs.getRequest(), ctx));
}
} finally {
lock.writeLock().unlock();
}
}
@Override
public void remove(byte[] token) {
try {
lock.writeLock().lock();
unsafeRemoveObservation(token);
} finally {
lock.writeLock().unlock();
}
}
/* *************** Observation utility functions **************** */
private org.eclipse.californium.core.observe.Observation unsafeGetObservation(Key token) {
org.eclipse.californium.core.observe.Observation obs = obsByToken.get(token);
return ObservationUtil.shallowClone(obs);
}
private void unsafeRemoveObservation(byte[] observationId) {
Key kToken = new Key(observationId);
org.eclipse.californium.core.observe.Observation removed = obsByToken.remove(kToken);
if (removed != null) {
String registrationId = extractRegistrationId(removed);
List<Key> tokens = tokensByRegId.get(registrationId);
tokens.remove(kToken);
if (tokens.isEmpty()) {
tokensByRegId.remove(registrationId);
}
}
}
private Collection<Observation> unsafeRemoveAllObservations(String registrationId) {
Collection<Observation> removed = new ArrayList<>();
List<Key> tokens = tokensByRegId.get(registrationId);
if (tokens != null) {
for (Key token : tokens) {
Observation observationRemoved = build(obsByToken.remove(token));
if (observationRemoved != null) {
removed.add(observationRemoved);
}
}
}
tokensByRegId.remove(registrationId);
return removed;
}
private Collection<Observation> unsafeGetObservations(String registrationId) {
Collection<Observation> result = new ArrayList<>();
List<Key> tokens = tokensByRegId.get(registrationId);
if (tokens != null) {
for (Key token : tokens) {
Observation obs = build(unsafeGetObservation(token));
if (obs != null) {
result.add(obs);
}
}
}
return result;
}
/* Retrieve the registrationId from the request context */
private String extractRegistrationId(org.eclipse.californium.core.observe.Observation observation) {
return observation.getRequest().getUserContext().get(CoapRequestBuilder.CTX_REGID);
}
private Observation build(org.eclipse.californium.core.observe.Observation cfObs) {
if (cfObs == null)
return null;
String regId = null;
String lwm2mPath = null;
Map<String, String> context = null;
for (Entry<String, String> ctx : cfObs.getRequest().getUserContext().entrySet()) {
switch (ctx.getKey()) {
case CTX_REGID:
regId = ctx.getValue();
break;
case CTX_LWM2M_PATH:
lwm2mPath = ctx.getValue();
break;
default:
if (context == null) {
context = new HashMap<>();
}
context.put(ctx.getKey(), ctx.getValue());
}
}
return new Observation(cfObs.getRequest().getToken(), regId, new LwM2mPath(lwm2mPath), context);
}
private void validateObservation(org.eclipse.californium.core.observe.Observation observation) {
if (!observation.getRequest().getUserContext().containsKey(CoapRequestBuilder.CTX_REGID))
throw new IllegalStateException("missing registrationId info in the request context");
if (!observation.getRequest().getUserContext().containsKey(CoapRequestBuilder.CTX_LWM2M_PATH))
throw new IllegalStateException("missing lwm2m path info in the request context");
if (getRegistration(observation.getRequest().getUserContext().get(CoapRequestBuilder.CTX_REGID)) == null) {
throw new IllegalStateException("no registration for this Id");
}
}
/* *************** Expiration handling **************** */
@Override
public void setExpirationListener(ExpirationListener listener) {
this.expirationListener = listener;
}
/**
* start the registration store, will start regular cleanup of dead registrations.
*/
@Override
public void start() {
schedExecutor.scheduleAtFixedRate(new Cleaner(), cleanPeriod, cleanPeriod, TimeUnit.SECONDS);
}
/**
* Stop the underlying cleanup of the registrations.
*/
@Override
public void stop() {
schedExecutor.shutdownNow();
try {
schedExecutor.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
LOG.warn("Clean up registration thread was interrupted.", e);
}
}
private class Cleaner implements Runnable {
@Override
public void run() {
try {
Collection<Registration> allRegs = new ArrayList<>();
try {
lock.readLock().lock();
allRegs.addAll(regsByEp.values());
} finally {
lock.readLock().unlock();
}
for (Registration reg : allRegs) {
if (!reg.isAlive()) {
// force de-registration
Deregistration removedRegistration = removeRegistration(reg.getId());
expirationListener.registrationExpired(removedRegistration.getRegistration(),
removedRegistration.getObservations());
}
}
} catch (Exception e) {
LOG.warn("Unexpected Exception while registration cleaning", e);
}
}
}
}