/* * Copyright 2014 Robert von Burg <eitch@eitchnet.ch> * * 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. */ package li.strolch.communication; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.BlockingDeque; import java.util.concurrent.LinkedBlockingDeque; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import li.strolch.communication.IoMessage.State; import li.strolch.utils.collections.MapOfLists; import li.strolch.utils.dbc.DBC; import li.strolch.utils.helper.StringHelper; /** * @author Robert von Burg <eitch@eitchnet.ch> */ public class CommunicationConnection implements Runnable { protected static final Logger logger = LoggerFactory.getLogger(CommunicationConnection.class); private String id; private ConnectionMode mode; private Map<String, String> parameters; private ConnectionState state; private String stateMsg; private BlockingDeque<IoMessage> messageQueue; private Thread queueThread; private volatile boolean run; private MapOfLists<CommandKey, ConnectionObserver> connectionObservers; private List<ConnectionStateObserver> connectionStateObservers; private CommunicationEndpoint endpoint; private IoMessageVisitor messageVisitor; private IoMessageArchive archive; public CommunicationConnection(String id, ConnectionMode mode, Map<String, String> parameters, CommunicationEndpoint endpoint, IoMessageVisitor messageVisitor) { DBC.PRE.assertNotEmpty("Id must be set!", id); //$NON-NLS-1$ DBC.PRE.assertNotNull("ConnectionMode must be set!", mode); //$NON-NLS-1$ DBC.PRE.assertNotNull("Paramerters must not be null!", parameters); //$NON-NLS-1$ DBC.PRE.assertNotNull("Endpoint must be set!", endpoint); //$NON-NLS-1$ DBC.PRE.assertNotNull("IoMessageVisitor must be set!", messageVisitor); //$NON-NLS-1$ this.id = id; this.mode = mode; this.parameters = parameters; this.endpoint = endpoint; this.messageVisitor = messageVisitor; this.state = ConnectionState.CREATED; this.stateMsg = this.state.toString(); this.messageQueue = new LinkedBlockingDeque<>(); this.connectionObservers = new MapOfLists<>(); this.connectionStateObservers = new ArrayList<>(); } public void setArchive(IoMessageArchive archive) { this.archive = archive; } public IoMessageArchive getArchive() { return this.archive; } public String getId() { return this.id; } public int getQueueSize() { return this.messageQueue.size(); } public ConnectionState getState() { return this.state; } public String getStateMsg() { return this.stateMsg; } public ConnectionMode getMode() { return this.mode; } public Map<String, String> getParameters() { return this.parameters; } public void clearQueue() { this.messageQueue.clear(); } public void addConnectionObserver(CommandKey key, ConnectionObserver observer) { synchronized (this.connectionObservers) { this.connectionObservers.addElement(key, observer); } } public void removeConnectionObserver(CommandKey key, ConnectionObserver observer) { synchronized (this.connectionObservers) { this.connectionObservers.removeElement(key, observer); } } public void addConnectionStateObserver(ConnectionStateObserver observer) { synchronized (this.connectionStateObservers) { this.connectionStateObservers.add(observer); } } public void removeConnectionStateObserver(ConnectionStateObserver observer) { synchronized (this.connectionStateObservers) { this.connectionStateObservers.remove(observer); } } public void notifyStateChange(ConnectionState state, String stateMsg) { ConnectionState oldState = this.state; String oldStateMsg = this.stateMsg; this.state = state; this.stateMsg = stateMsg; List<ConnectionStateObserver> observers; synchronized (this.connectionStateObservers) { observers = new ArrayList<>(this.connectionStateObservers); } for (ConnectionStateObserver observer : observers) { observer.notify(oldState, oldStateMsg, state, stateMsg); } } public void switchMode(ConnectionMode mode) { ConnectionMessages.assertConfigured(this, "Can not switch modes yet!"); //$NON-NLS-1$ if (mode == ConnectionMode.OFF) { stop(); } else if (mode == ConnectionMode.ON) { stop(); start(); } this.mode = mode; } /** * Configure the underlying {@link CommunicationEndpoint} and {@link IoMessageVisitor} */ public void configure() { this.messageVisitor.configure(this); this.endpoint.configure(this, this.messageVisitor); notifyStateChange(ConnectionState.INITIALIZED, ConnectionState.INITIALIZED.name()); } public void start() { ConnectionMessages.assertConfigured(this, "Can not start yet!"); //$NON-NLS-1$ switch (this.mode) { case OFF: logger.info("Not connecting as mode is currently OFF"); //$NON-NLS-1$ break; case SIMULATION: logger.info("Started SIMULATION connection!"); //$NON-NLS-1$ break; case ON: if (this.queueThread != null) { logger.warn(MessageFormat.format("{0}: Already connected!", this.id)); //$NON-NLS-1$ } else { logger.info(MessageFormat.format("Starting Connection {0} to {1}...", this.id, getRemoteUri())); //$NON-NLS-1$ this.run = true; this.queueThread = new Thread(this, MessageFormat.format("{0}_OUT", this.id)); //$NON-NLS-1$ this.queueThread.start(); connectEndpoint(); } break; default: logger.error("Unhandled mode " + this.mode); //$NON-NLS-1$ break; } } public void stop() { ConnectionMessages.assertConfigured(this, "Can not stop yet!"); //$NON-NLS-1$ switch (this.mode) { case OFF: break; case SIMULATION: logger.info("Disconnected SIMULATION connection!"); //$NON-NLS-1$ break; case ON: logger.info("Disconnecting..."); //$NON-NLS-1$ if (this.queueThread == null) { logger.warn(MessageFormat.format("{0}: Already disconnected!", this.id)); //$NON-NLS-1$ } else { this.run = false; try { disconnectEndpoint(); } catch (Exception e) { String msg = "Caught exception while disconnecting endpoint: {0}"; //$NON-NLS-1$ logger.error(MessageFormat.format(msg, e.getLocalizedMessage()), e); } try { this.queueThread.interrupt(); } catch (Exception e) { String msg = "Caught exception while stopping queue thread: {0}"; //$NON-NLS-1$ logger.warn(MessageFormat.format(msg, e.getLocalizedMessage())); } String msg = "{0} is stopped"; //$NON-NLS-1$ logger.info(MessageFormat.format(msg, this.queueThread.getName())); this.queueThread = null; } break; default: logger.error("Unhandled mode " + this.mode); //$NON-NLS-1$ break; } } /** * Called by the underlying endpoint when a new message has been received and parsed * * @param message */ public void handleNewMessage(IoMessage message) { ConnectionMessages.assertConfigured(this, "Can not be notified of new message yet!"); //$NON-NLS-1$ // if the state of the message is already later than ACCEPTED // then an underlying component has already set the state, so // we don't need to set it if (message.getState().compareTo(State.ACCEPTED) < 0) message.setState(State.ACCEPTED, StringHelper.DASH); notifyObservers(message); } public void notifyObservers(IoMessage message) { List<ConnectionObserver> observers; synchronized (this.connectionObservers) { List<ConnectionObserver> list = this.connectionObservers.getList(message.getKey()); if (list == null) return; observers = new ArrayList<>(list); } for (ConnectionObserver observer : observers) { try { observer.notify(message.getKey(), message); } catch (Exception e) { String msg = "Failed to notify observer for key {0} on message with id {1}"; //$NON-NLS-1$ logger.error(MessageFormat.format(msg, message.getKey(), message.getId()), e); } } if (this.archive != null) this.archive.archive(message); } @Override public void run() { while (this.run) { IoMessage message = null; try { message = this.messageQueue.take(); logger.info(MessageFormat.format("Processing message {0}...", message.getId())); //$NON-NLS-1$ if (this.mode == ConnectionMode.ON) this.endpoint.send(message); else if (this.mode == ConnectionMode.SIMULATION) this.endpoint.simulate(message); // notify the caller that the message has been processed if (message.getState().compareTo(State.DONE) < 0) message.setState(State.DONE, StringHelper.DASH); done(message); } catch (InterruptedException e) { logger.warn(MessageFormat.format("{0} connection has been interruped!", this.id)); //$NON-NLS-1$ // an interrupted exception means the thread must stop this.run = false; if (message != null) { logger.error(MessageFormat.format("Can not send message {0}", message.getId())); //$NON-NLS-1$ message.setState(State.FATAL, e.getLocalizedMessage()); done(message); } } catch (Exception e) { logger.error(e.getMessage(), e); if (message != null) { logger.error(MessageFormat.format("Can not send message {0}", message.getId())); //$NON-NLS-1$ message.setState(State.FATAL, e.getLocalizedMessage()); done(message); } } finally { if (message != null && this.archive != null) { this.archive.archive(message); } } } } /** * Called when an outgoing message has been handled. This method logs the message state and then notifies all * observers * * @param message */ public void done(IoMessage message) { ConnectionMessages.assertConfigured(this, "Can not notify observers yet!"); //$NON-NLS-1$ switch (message.getState()) { case ACCEPTED: case CREATED: case DONE: case PENDING: logger.info(MessageFormat.format("Sent message {0}", message.toString())); //$NON-NLS-1$ break; case FAILED: case FATAL: logger.error(MessageFormat.format("Failed to send message {0}", message.toString())); //$NON-NLS-1$ break; default: logger.error(MessageFormat.format("Unhandled state for message {0}", message.toString())); //$NON-NLS-1$ break; } notifyObservers(message); } public String getRemoteUri() { return this.endpoint == null ? "0.0.0.0:0" : this.endpoint.getRemoteUri(); //$NON-NLS-1$ } public String getLocalUri() { return this.endpoint == null ? "0.0.0.0:0" : this.endpoint.getLocalUri(); //$NON-NLS-1$ } public void reset() { ConnectionMessages.assertConfigured(this, "Can not resest yet!"); //$NON-NLS-1$ this.endpoint.reset(); } /** * Called when the connection is connected, thus the underlying endpoint can be started */ protected void connectEndpoint() { this.endpoint.start(); } /** * Called when the connection is disconnected, thus the underlying endpoint must be stopped */ protected void disconnectEndpoint() { this.endpoint.stop(); } /** * Send the message using the underlying endpoint. Do not change the state of the message, this will be done by the * caller * * @param message */ public void send(IoMessage message) { ConnectionMessages.assertConfigured(this, "Can not send yet"); //$NON-NLS-1$ if (this.mode == ConnectionMode.OFF) throw ConnectionMessages.throwNotConnected(this, message); message.setState(State.PENDING, State.PENDING.name()); this.messageQueue.add(message); this.messageQueue.add(message); } }