/*
* Syncany, www.syncany.org
* Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.syncany.operations.watch;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.syncany.util.StringUtil;
/**
* The notification listener implements a client to the fanout, as very
* lightweight pub/sub server originally written for SparkleShare.
*
* <p>Fanout implements a simple TCP-based plaintext protocol.<br />
* It implements the following <b>commands</b>:
* <ul>
* <li><tt>subcribe <channel></tt></li>
* <li><tt>unsubscribe <channel></tt></li>
* <li><tt>announce <channel> <message></tt></li>
* </ul>
*
* <p><b>Notifications</b> have the following format:
* <ul>
* <li><tt><channel>!<message></tt></li>
* </ul>
*
* <p>The notification listener starts a thread and listens for incoming messages.
* Outgoing messages (subscribe/unsubscribe/announce) are sent directly or (if that
* fails), put in an outgoing queue. Incoming messages are handed over to a
* {@link NotificationListenerListener}.
*
* @see <a href="https://github.com/travisghansen/fanout/">https://github.com/travisghansen/fanout/</a> - Fanout source code by Travis G. Hansen
* @author Philipp C. Heckel <philipp.heckel@gmail.com>
*/
public class NotificationListener {
private static final Logger logger = Logger.getLogger(NotificationListener.class.getSimpleName());
private static final int SOCKET_TIMEOUT = 10000;
private static final int RECONNECT_WAIT_TIME = 5000;
private String host;
private int port;
private NotificationListenerListener listener;
private AtomicBoolean connected;
private AtomicBoolean running;
private Socket socket;
private OutputStream socketOut;
private BufferedReader socketIn;
private Set<String> subscribedChannels;
private Queue<String> outgoingMessageQueue;
private Thread incomingMessageThread;
public NotificationListener(String host, int port, NotificationListenerListener listener) {
this.host = host;
this.port = port;
this.listener = listener;
this.subscribedChannels = new HashSet<String>();
this.incomingMessageThread = null;
this.outgoingMessageQueue = new LinkedList<String>();
this.connected = new AtomicBoolean(false);
this.running = new AtomicBoolean(false);
}
public void start() {
logger.log(Level.INFO, "Starting notification listener thread ...");
stop();
incomingMessageThread = new SocketThread();
incomingMessageThread.start();
}
public void stop() {
if (incomingMessageThread != null) {
logger.log(Level.INFO, "Stopping notification listener thread ...");
try {
running.set(false);
if (socket != null) {
socket.close();
}
if (incomingMessageThread != null) {
incomingMessageThread.interrupt();
}
}
catch (IOException e) {
logger.log(Level.FINE, "Could not close the socket", e);
}
finally {
incomingMessageThread = null;
}
}
}
public void subscribe(String channel) {
subscribedChannels.add(channel);
logger.log(Level.INFO, "Subscribing to channel " + channel + "...");
sendMessageOrAddToOutgoingQueue(String.format("subscribe %s\n", channel));
}
public void unsubscribe(String channel) {
subscribedChannels.remove(channel);
logger.log(Level.INFO, "Unsubscribing from channel " + channel + "...");
sendMessageOrAddToOutgoingQueue(String.format("unsubscribe %s\n", channel));
}
public void announce(String channel, String message) {
logger.log(Level.INFO, "Announcing to channel " + channel + ": " + message.trim());
sendMessageOrAddToOutgoingQueue(String.format("announce %s %s\n", channel, message));
}
private void sendMessageOrAddToOutgoingQueue(String message) {
if (connected.get()) {
try {
socketOut.write(StringUtil.toBytesUTF8(message));
logger.log(Level.INFO, "Sent message: " + message.trim());
}
catch (IOException e) {
logger.log(Level.FINE, "Could write to the socket", e);
queueOutgoingMessage(message);
}
}
else {
queueOutgoingMessage(message);
}
}
private void queueOutgoingMessage(String message) {
if (!outgoingMessageQueue.contains(message)) {
logger.log(Level.INFO, "Sending failed or no connection, queuing message: " + message.trim());
outgoingMessageQueue.offer(message);
}
else {
logger.log(Level.INFO, "Sending failed and message already in queue: " + message.trim());
}
}
private void connect() {
try {
logger.log(Level.INFO, "Connecting socket to " + host + ":" + port + " ...");
socket = new Socket(host, port);
socket.setSoTimeout(SOCKET_TIMEOUT);
socketOut = socket.getOutputStream();
socketIn = new BufferedReader(new InputStreamReader(socket.getInputStream()));
connected.set(socket.isConnected());
}
catch (IOException e) {
logger.log(Level.FINE, "Could not connect the socket", e);
disconnect();
}
}
private void disconnect() {
try {
logger.log(Level.INFO, "Disconnecting socket ...");
if (socket != null) {
socket.close();
}
if (socketOut != null) {
socketOut.close();
}
if (socketIn != null) {
socketIn.close();
}
}
catch (IOException e) {
logger.log(Level.FINE, "Could not close the socket", e);
}
finally {
socket = null;
socketIn = null;
socketOut = null;
connected.set(false);
}
}
private class SocketThread extends Thread {
public SocketThread() {
super("NotifyThread");
}
@Override
public void run() {
running.set(true);
connect();
while (running.get()) {
try {
if (socket == null || socketIn == null) {
throw new Exception("Socket closed");
}
if (outgoingMessageQueue.size() > 0) {
logger.log(Level.INFO, "Processing queued outgoing messages ...");
processOutgoingMessages();
}
logger.log(Level.INFO, "Waiting for incoming message (" + SOCKET_TIMEOUT + " ms) ...");
processIncomingMessage(socketIn.readLine());
}
catch (SocketTimeoutException e) {
// Nothing. Do not log the exception either.
//logger.log(Level.FINE, "Socket timed out", e);
}
catch (InterruptedException e) {
logger.log(Level.INFO, "Notification listener interrupted.", e);
running.set(false);
}
catch (Exception e) {
try {
logger.log(Level.INFO, "Notification connection down: " + e.getMessage() + ", sleeping " + RECONNECT_WAIT_TIME
+ "ms, then trying a re-connect ...");
Thread.sleep(RECONNECT_WAIT_TIME);
connect();
if (subscribedChannels.size() > 0) {
logger.log(Level.INFO, "Re-subscribing to channels after broken connection ...");
for (String channel : subscribedChannels) {
subscribe(channel);
}
}
}
catch (InterruptedException e2) {
logger.log(Level.INFO, "Notification listener interrupted.", e2);
running.set(false);
}
}
}
logger.log(Level.INFO, "STOPPED notification listener!");
}
private void processOutgoingMessages() throws IOException {
String nextMessage = null;
while (null != (nextMessage = outgoingMessageQueue.poll())) {
socketOut.write(StringUtil.toBytesUTF8(nextMessage));
logger.log(Level.INFO, "- Sent queued message " + nextMessage);
}
}
private void processIncomingMessage(String messageLine) {
String[] messageParts = messageLine.split("!");
if (messageParts.length == 2) {
String channel = messageParts[0];
String message = messageParts[1];
if (!"debug".equals(channel)) {
logger.log(Level.INFO, "Received message for channel " + channel + ": " + message);
listener.pushNotificationReceived(channel, message);
}
}
}
}
public interface NotificationListenerListener {
public void pushNotificationReceived(String channel, String message);
}
}