/** * Copyright (c) 2010-2016 by the respective copyright holders. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.lcn.connection; import java.io.IOException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import org.openhab.binding.lcn.common.LcnBindingNotification; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class manages all configured connections to LCN-PCHK. * * @author Tobias J�ttner */ public class ConnectionManager implements Connection.Callback, Runnable { /** Logger for this class. */ private static final Logger logger = LoggerFactory.getLogger(ConnectionManager.class); /** Must be implemented by the owner. */ public interface Callback { /** * Runs a notification on the refresh thread. * * @param n the notification */ void runOnRefreshThreadAsync(LcnBindingNotification n); /** * Updates all openHAB items associated with the given connection. * * @param conn the connection */ void updateItems(Connection conn); /** * Process input received from the given connection. * * @param input the received input * @param conn the source connection */ void onInputReceived(String input, Connection conn); /** * Get all connection settings. * * @return the settings */ Map<String, ConnectionSettings> getAllSets(); } /** Time to wait before a reconnect attempt (in milliseconds). */ private static final int RECONNECT_INTERVAL_MSEC = 5000; /** Used to detect closed channels. */ private static final int DETECT_CLOSED_CHANNELS_INTERVAL_MSEC = 10000; /** The callback to the owner. */ private final Callback callback; /** Thread for connecting and reading from all connections. */ private Thread thread; /** Termination flag for for the thread. */ private volatile boolean threadTreminate = false; /** Selector used to handle channel events. */ private Selector selector; /** * Sync. object used to register new channels. * Will hinder the {@link #selector} to enter a new {@link Selector#select()} while the channel is still * registering. */ private final Object channelRegisterSync = new Object(); /** List of all connections stored by their unique identifier (upper-case). */ private final HashMap<String, Connection> connectionsById = new HashMap<String, Connection>(); /** * Constructs a connection manager with the given owner callback. * * @param callback the owner */ public ConnectionManager(Callback callback) { this.callback = callback; } /** * Adds a new connection. * * @param conn the connection to add */ public void add(Connection conn) { this.connectionsById.put(conn.getSets().getId().toUpperCase(), conn); } /** * Closes and removes the given connection. * * @param id the connection's unique identifier * @return true on success */ public boolean disconnectAndRemove(String id) { Connection conn = this.connectionsById.get(id.toUpperCase()); if (conn == null) { return false; } conn.disconnect(); this.connectionsById.remove(id); return true; } /** * Find a connection by its unique identifier. * * @param id the connection to search for * @return the found connection or null */ public Connection get(String id) { return this.connectionsById.get(id.toUpperCase()); } /** Notifies that the binding has started. */ public void activate() { try { this.selector = Selector.open(); this.thread = new Thread(this); this.thread.start(); } catch (IOException e) { logger.error("Unable to open the Selector!"); } } /** Notifies that the binding has stopped. */ public void deactivate() { try { this.threadTreminate = true; this.selector.close(); this.thread.join(); } catch (InterruptedException ex) { } catch (IOException e) { logger.error("Unable to close the Selector!"); } } /** Tells all connections to flush their queued data. */ public void flush() { for (Connection conn : this.connectionsById.values()) { conn.flush(); } } /** * Updates the managed connections. * Obsolete connections are closed and removed, while new and updated ones are (re-)established. * Finally all connections are told to update themselves. */ public void update() { // Close and remove changed and no longer existing connections HashSet<String> removeIds = new HashSet<String>(); for (Connection conn : this.connectionsById.values()) { ConnectionSettings sets = this.callback.getAllSets().get(conn.getSets().getId().toUpperCase()); if (sets != null) { if (!sets.equals(conn.getSets())) { removeIds.add(conn.getSets().getId()); } } else { removeIds.add(conn.getSets().getId()); } } for (String id : removeIds) { this.disconnectAndRemove(id); } // Add new connections for (ConnectionSettings sets : this.callback.getAllSets().values()) { if (!this.connectionsById.containsKey(sets.getId().toUpperCase())) { Connection conn = new Connection(sets, this); this.add(conn); conn.beginConnect(); } } // Finally update now existing connections for (Connection conn : this.connectionsById.values()) { conn.update(); } } /** {@inheritDoc} */ @Override public Selector getSelector() { return this.selector; } /** {@inheritDoc} */ @Override public void updateItems(Connection conn) { this.callback.updateItems(conn); } /** {@inheritDoc} */ @Override public void onInputReceived(String input, Connection conn) { this.callback.onInputReceived(input, conn); } /** {@inheritDoc} */ @Override public Object getChannelRegisterSync() { return this.channelRegisterSync; } /** * Main method of the thread. * Establishes new connections and waits for input data. */ @Override public void run() { final Object sync = new Object(); long lastCloseDetectTime = System.nanoTime(); while (!this.threadTreminate) { long currTime = System.nanoTime(); try { int timeoutMSec = DETECT_CLOSED_CHANNELS_INTERVAL_MSEC - (int) ((currTime - lastCloseDetectTime) / 1000000L); if (timeoutMSec <= 0 || this.selector.select(timeoutMSec) == 0) { // Detect closed channels synchronized (sync) { this.callback.runOnRefreshThreadAsync(new LcnBindingNotification() { @Override public void execute() { for (Connection conn : ConnectionManager.this.connectionsById.values()) { if (!conn.isChannelConnected() && !conn.isChannelConnecting()) { logger.warn(String.format( "Channel \"%s\" was closed unexpectedly (reconnecting...).", conn.getSets().getId())); conn.beginReconnect(RECONNECT_INTERVAL_MSEC); } } synchronized (sync) { sync.notify(); } } }); sync.wait(); } lastCloseDetectTime = currTime; } else { // Process selected keys (connect or read events) Iterator<SelectionKey> iter = this.selector.selectedKeys().iterator(); while (iter.hasNext()) { final SelectionKey key = iter.next(); // The invocation is done sync. to keep the SelectionKey valid synchronized (sync) { this.callback.runOnRefreshThreadAsync(new LcnBindingNotification() { @Override public void execute() { Connection conn = (Connection) key.attachment(); try { if (key.isConnectable()) { conn.finishConnect(); } else if (key.isReadable()) { conn.readAndProcess(); } } catch (IOException ex) { logger.warn(String.format("Cannot process channel \"%s\" (reconnecting...): %s", conn.getSets().getId(), ex.getMessage())); conn.beginReconnect(RECONNECT_INTERVAL_MSEC); } synchronized (sync) { sync.notify(); } } }); sync.wait(); } iter.remove(); } } synchronized (this.channelRegisterSync) { } // Force a wait here if a new channel is registering } catch (InterruptedException ex) { } catch (IOException ex) { logger.error("Selection failure: " + ex.getMessage()); } } } }