package io.eguan.srv;
/*
* #%L
* Project eguan
* %%
* Copyright (C) 2012 - 2017 Oodrive
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.GuardedBy;
import javax.management.AttributeChangeNotification;
import javax.management.ListenerNotFoundException;
import javax.management.MBeanNotificationInfo;
import javax.management.Notification;
import javax.management.NotificationBroadcasterSupport;
import javax.management.NotificationEmitter;
import javax.management.NotificationFilter;
import javax.management.NotificationListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract class for a server of devices. The server binds on a given TCP/IP address and port. The managed devices must
* implement the interface
*
* @author oodrive
* @author llambert
* @author ebredzinski
*
* @param <S>
* TCP/IP server, implementing some protocol to access to the device. The main loop must be implemented as a
* {@link Callable}.
* @param <T>
* target representing a device.
*/
public abstract class AbstractServer<S extends Callable<Void>, T extends DeviceTarget, K extends AbstractServerConfig>
implements NotificationEmitter, AbstractServerMXBean {
/** Package logger */
static final Logger LOGGER = LoggerFactory.getLogger(AbstractServer.class.getPackage().getName());
// Control timeout to wait for the server start or end
private static final int LOOP_COUNTER_LIMIT = 100;
private static final int LOOP_WAIT_TIME = 100;
private static final Comparator<String> IGNORECASE_COMPARATOR = new Comparator<String>() {
@Override
public final int compare(final String s1, final String s2) {
return s1.compareToIgnoreCase(s2);
}
};
/** Server config */
private volatile K serverConfig;
/** Started server config */
private volatile K serverConfigStarted;
/** Executor to run the server */
private final ExecutorService serverExecutor;
/** Config lock. Share access to target list and config */
private final ReadWriteLock serverLock = new ReentrantReadWriteLock();
/** Current server future task or <code>null</code> */
@GuardedBy(value = "serverLock")
private Future<Void> serverFuture;
/** Current targets. The key is the TargetName and the value is the target. */
@GuardedBy(value = "serverLock")
private final Map<String, T> targets = new TreeMap<String, T>(IGNORECASE_COMPARATOR);
/** JMX notifications support */
private final NotificationBroadcasterSupport notificationEmitter = new NotificationBroadcasterSupport();
/** JMX notification sequence number */
private final AtomicInteger notificationSequenceNumber = new AtomicInteger(1);
/**
* Create a new server that will bind on the given address and port.
*
* @param displayName
* display name of protocol or server
*/
protected AbstractServer(@Nonnull final K serverConfig, final String displayName) {
this.serverConfig = serverConfig;
this.serverExecutor = Executors.newFixedThreadPool(1, new ThreadFactory() {
/**
* Create a non daemon thread to run the server.
*
* @see java.util.concurrent.ThreadFactory#newThread(java.lang.Runnable)
*/
@Override
public final Thread newThread(final Runnable r) {
final Thread thread = new Thread(r, displayName + " server");
thread.setDaemon(true);
thread.setPriority(Thread.NORM_PRIORITY + 2);
return thread;
}
});
}
/**
* Gets the current server config
*
* @return the current config
*/
protected final K getServerConfig() {
return serverConfig;
}
/**
* Gets the server bind address.
*
* @return the bind address.
*/
public final InetAddress getInetAddress() {
return serverConfig.getAddress();
}
/*
* (non-Javadoc)
*
* @see io.eguan.srv.AbstractServerMXBean#getAddress()
*/
@Override
public final String getAddress() {
return serverConfig.getAddress().getHostAddress();
}
/**
* Sets the server bind address. Will be taken into account during the next server start.
*
* @param address
* new server bind address.
* @throws NullPointerException
* if address is <code>null</code>
* @throws ServerConfigurationException
* if address is invalid (not a local address)
*/
public final void setAddress(@Nonnull final InetAddress address) throws NullPointerException,
ServerConfigurationException {
setAddress(address, true);
}
private final void setAddress(@Nonnull final InetAddress address, final boolean notify)
throws NullPointerException, ServerConfigurationException {
final InetAddress addressOld = this.serverConfig.getAddress();
this.serverConfig.setAddress(address);
// Send JMX notification
if (notify) {
final Notification n = new AttributeChangeNotification(this, notificationSequenceNumber.getAndIncrement(),
System.currentTimeMillis(), "Address changed", "Address", "InetAddress", addressOld, address);
notificationEmitter.sendNotification(n);
}
}
/*
* (non-Javadoc)
*
* @see io.eguan.srv.AbstractServerMXBean#setAddress(java.lang.String)
*/
@Override
public final void setAddress(@Nonnull final String address) throws UnknownHostException {
Objects.requireNonNull(address);
setAddress(InetAddress.getByName(address));
}
/*
* (non-Javadoc)
*
* @see io.eguan.srv.AbstractServerMXBean#getPort()
*/
@Override
public final int getPort() {
return serverConfig.getPort();
}
/*
* (non-Javadoc)
*
* @see io.eguan.srv.AbstractServerMXBean#setPort(int)
*/
@Override
public final void setPort(final int port) throws ServerConfigurationException {
setPort(port, true);
}
private final void setPort(final int port, final boolean notify) throws ServerConfigurationException {
final int oldPort = this.serverConfig.getPort();
this.serverConfig.setPort(port);
// Send JMX notification
if (notify) {
final Notification n = new AttributeChangeNotification(this, notificationSequenceNumber.getAndIncrement(),
System.currentTimeMillis(), "Port changed", "Port", "int", Integer.valueOf(oldPort),
Integer.valueOf(port));
notificationEmitter.sendNotification(n);
}
}
/*
* (non-Javadoc)
*
* @see io.eguan.srv.AbstractServerMXBean#isRestartRequired
*/
@Override
public final boolean isRestartRequired() {
return serverConfigStarted != null && !getServerConfig().equals(serverConfigStarted);
}
/*
* (non-Javadoc)
*
* @see io.eguan.srv.AbstractServerMXBean#start()
*/
@SuppressWarnings("unchecked")
@Override
public final void start() throws IllegalStateException {
final K serverConfigTemp;
serverLock.writeLock().lock();
try {
LOGGER.debug("Starting " + this);
if (isStarted()) {
throw new IllegalStateException("Started");
}
// Create a new server for the current configuration
serverConfigTemp = (K) serverConfig.clone();
final S server = createServer(serverConfigTemp);
serverFuture = serverExecutor.submit(server);
}
finally {
serverLock.writeLock().unlock();
}
// Wait for the server to start: make sure the configuration is loaded
for (int counter = 0; counter < LOOP_COUNTER_LIMIT; counter++) {
Thread.yield();
try {
Thread.sleep(LOOP_WAIT_TIME);
}
catch (final InterruptedException e) {
// ignored
}
serverLock.readLock().lock();
try {
if (isServerStarted()) {
// Save configuration of the server
serverConfigStarted = serverConfigTemp;
// Notify start of server
final Notification n = new AttributeChangeNotification(this,
notificationSequenceNumber.getAndIncrement(), System.currentTimeMillis(),
"Started changed", "Started", "boolean", Boolean.FALSE, Boolean.TRUE);
notificationEmitter.sendNotification(n);
return;
}
}
finally {
serverLock.readLock().unlock();
}
}
// Not started nor stopped nor aborted!
LOGGER.warn("Server start: wait timeout");
}
/*
* (non-Javadoc)
*
* @see io.eguan.srv.AbstractServerMXBean#stop()
*/
@Override
public final void stop() {
serverLock.writeLock().lock();
try {
LOGGER.debug("Stopping " + toString());
if (serverFuture != null) {
try {
// Cancel task and server if it's running
serverFuture.cancel(false);
serverCancel();
// Sleep at least once to make sure that the resources (TCP connections)
// have been released
for (int counter = 0; counter < LOOP_COUNTER_LIMIT; counter++) {
Thread.yield();
Thread.sleep(LOOP_WAIT_TIME);
if (serverFuture.isDone()) {
break;
}
}
}
catch (final Exception e) {
// Ignored
LOGGER.warn("Error while stopping " + toString(), e);
}
finally {
serverFuture = null;
}
serverConfigStarted = null;
// Notify stop of server
final Notification n = new AttributeChangeNotification(this,
notificationSequenceNumber.getAndIncrement(), System.currentTimeMillis(), "Started changed",
"Started", "boolean", Boolean.TRUE, Boolean.FALSE);
notificationEmitter.sendNotification(n);
}
LOGGER.debug("Stopping " + toString() + " done");
}
finally {
serverLock.writeLock().unlock();
}
}
/*
* (non-Javadoc)
*
* @see io.eguan.srv.AbstractServerMXBean#isStarted()
*/
@Override
public final boolean isStarted() {
serverLock.readLock().lock();
try {
return !(serverFuture == null || serverFuture.isDone());
}
finally {
serverLock.readLock().unlock();
}
}
/**
* Creates a new server instance.
*
* @return the new server.
*/
protected abstract S createServer(K serverConfig);
/**
* Tells if the server is started.
*
* @return <code>true</code> if the server has entered its main loop.
*/
protected abstract boolean isServerStarted();
/**
* Cancels the server.
*/
protected abstract void serverCancel();
/**
* Notify that the server have been stopped.
*/
protected abstract void serverStopped();
/**
* Add a target device to the server.
*
* @param target
* the target to add.
* @return the previous target that had the same TargetName or <code>null</code>
*/
public final T addTarget(@Nonnull final T target) {
serverLock.writeLock().lock();
try {
final T prev = targets.put(target.getTargetName(), target);
targetAdded(target, prev);
return prev;
}
finally {
serverLock.writeLock().unlock();
}
}
/**
* Remove a target device from the server.
*
* @param targetName
* name of the target to remove
* @return the removed target or <code>null</code> if no target have this name
*/
public final T removeTarget(@Nonnull final String targetName) {
serverLock.writeLock().lock();
try {
final T removed = targets.remove(Objects.requireNonNull(targetName));
targetRemoved(targetName, removed);
return removed;
}
finally {
serverLock.writeLock().unlock();
}
}
/**
* Lock to take to read the target map.
*
* @return the lock to read the target map.
*/
protected final Lock getTargetSharedLock() {
return serverLock.readLock();
}
/**
* Read-only view of the targets. Must take the target shared lock to avoid a concurrent access from another thread.
*
* @return the targets by name.
*/
protected final Map<String, T> getTargetMap() {
return Collections.unmodifiableMap(targets);
}
/**
* Notify that a new target have been added.
*
* @param targetNew
* the added target
* @param targetPrev
* the previous target with the same name. May be <code>null</code>
*/
protected abstract void targetAdded(final T targetNew, final T targetPrev);
/**
* Notify that a target have been removed.
*
* @param name
* name of the target to remove.
* @param target
* the target found in the local cache (may be <code>null</code>)
*/
protected abstract void targetRemoved(final String name, final T target);
/*
* (non-Javadoc)
*
* @see javax.management.NotificationBroadcaster#addNotificationListener(javax.management.NotificationListener,
* javax.management.NotificationFilter, java.lang.Object)
*/
@Override
public final void addNotificationListener(final NotificationListener listener, final NotificationFilter filter,
final Object handback) throws IllegalArgumentException {
notificationEmitter.addNotificationListener(listener, filter, handback);
}
/*
* (non-Javadoc)
*
* @see javax.management.NotificationEmitter#removeNotificationListener(javax.management.NotificationListener,
* javax.management.NotificationFilter, java.lang.Object)
*/
@Override
public final void removeNotificationListener(final NotificationListener listener, final NotificationFilter filter,
final Object handback) throws ListenerNotFoundException {
notificationEmitter.removeNotificationListener(listener, filter, handback);
}
/*
* (non-Javadoc)
*
* @see javax.management.NotificationBroadcaster#removeNotificationListener(javax.management.NotificationListener)
*/
@Override
public final void removeNotificationListener(final NotificationListener listener) throws ListenerNotFoundException {
notificationEmitter.removeNotificationListener(listener);
}
/*
* (non-Javadoc)
*
* @see javax.management.NotificationBroadcaster#getNotificationInfo()
*/
@Override
public final MBeanNotificationInfo[] getNotificationInfo() {
final String[] types = new String[] { AttributeChangeNotification.ATTRIBUTE_CHANGE };
final String name = AttributeChangeNotification.class.getName();
final String description = "An attribute has changed";
final MBeanNotificationInfo info = new MBeanNotificationInfo(types, name, description);
return new MBeanNotificationInfo[] { info };
}
/**
* Gets notification broadcaster support.
*
* @return the notification emitter
*/
protected final NotificationBroadcasterSupport getNotificationBroadcasterSupport() {
return notificationEmitter;
}
/**
* Gets a new sequence number.
*
* @return the new sequence number
*/
protected final int getNotificationSequenceNumber() {
return notificationSequenceNumber.getAndIncrement();
}
}