/**
*
* Copyright (c) 2009-2016 Freedomotic team http://freedomotic.com
*
* This file is part of Freedomotic
*
* 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 2, 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
* Freedomotic; see the file COPYING. If not, see
* <http://www.gnu.org/licenses/>.
*/
package com.freedomotic.api;
import com.freedomotic.exceptions.PluginRuntimeException;
import com.freedomotic.events.PluginHasChanged;
import com.freedomotic.exceptions.PluginShutdownException;
import com.freedomotic.exceptions.PluginStartupException;
import com.freedomotic.exceptions.UnableToExecuteException;
import com.freedomotic.reactions.Command;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.ObjectMessage;
/**
* Uses a Template Method pattern which allows subclass to define how to perform
* a command or how to act when a specific event is received.
*/
public abstract class Protocol extends Plugin {
private static final Logger LOG = LoggerFactory.getLogger(Protocol.class.getName());
private int pollingWaitTime = -1;
private Protocol.SensorThread sensorThread;
private volatile Destination lastDestination;
/**
*
* @param pluginName
* @param manifest
*/
public Protocol(String pluginName, String manifest) {
super(pluginName, manifest);
setStatus(PluginStatus.STOPPED);
}
/**
*
* @throws com.freedomotic.exceptions.PluginRuntimeException
*/
protected abstract void onRun() throws PluginRuntimeException;
/**
*
* @param c
* @throws IOException
* @throws UnableToExecuteException
*/
protected abstract void onCommand(Command c) throws IOException, UnableToExecuteException;
/**
*
* @param c
* @return
*/
protected abstract boolean canExecute(Command c);
/**
*
* @param event
*/
protected abstract void onEvent(EventTemplate event);
/**
*
* @param listento
*/
public void addEventListener(String listento) {
listener.consumeEventFrom(listento);
}
public void removeEventListeners() {
listener.destroy();
}
/**
*
* @param ev
*/
public void notifyEvent(EventTemplate ev) {
if (isAllowedToSend()) {
notifyEvent(ev, ev.getDefaultDestination());
}
}
public Command notifyCommand(Command command) {
return getBusService().send(command);
}
/**
*
* @param ev
* @param destination
*/
public void notifyEvent(EventTemplate ev, String destination) {
if (isAllowedToSend()) {
if (LOG.isDebugEnabled()) {
LOG.debug("Sensor \"" + this.getName() + "\" notifies event \"" + ev.getEventName() + "\" with payload \"" + ev.getPayload().toString() + "\"");
}
getBusService().send(ev, destination);
}
}
/**
*
*/
@Override
public void start() {
super.start();
if (isAllowedToStart()) {
LOG.info("Starting plugin \"{}\"", getName());
Runnable action = new Runnable() {
@Override
public synchronized void run() {
try {
setStatus(PluginStatus.STARTING);
//onStart() is called before the thread because it may have some initialization for the sensor thread
try {
onStart();
} catch (PluginStartupException startupEx) {
notifyCriticalError(startupEx.getMessage(), startupEx);
return; //stop the plugin startup
}
sensorThread = new Protocol.SensorThread();
sensorThread.start();
setStatus(PluginStatus.RUNNING);
PluginHasChanged event = new PluginHasChanged(this, getName(), PluginHasChanged.PluginActions.START);
getBusService().send(event);
} catch (Exception e) {
setStatus(PluginStatus.FAILED);
setDescription("Plugin starting FAILED. see logs for details.");
LOG.error("Plugin ''" + getName() + "'' starting FAILED: " + e.getLocalizedMessage(), e);
}
}
};
getApi().getAuth().pluginBindRunnablePrivileges(this, action).run();
}
}
/**
*
*/
@Override
public void stop() {
super.stop();
if (isRunning()) {
LOG.info("Stopping plugin \"{}\"", getName());
Runnable action = new Runnable() {
@Override
public synchronized void run() {
try {
setStatus(PluginStatus.STOPPING);
try {
onStop();
} catch (PluginShutdownException shutdownEx) {
notifyError(shutdownEx.getMessage());
}
sensorThread = null;
PluginHasChanged event = new PluginHasChanged(this, getName(), PluginHasChanged.PluginActions.STOP);
getBusService().send(event);
setStatus(PluginStatus.STOPPED);
} catch (Exception e) {
setStatus(PluginStatus.FAILED);
setDescription("Plugin stopping FAILED. see logs for details.");
LOG.error("Error stopping plugin \"" + getName() + "\": " + e.getLocalizedMessage(), e);
}
}
};
getApi().getAuth().pluginBindRunnablePrivileges(this, action).run();
}
}
/**
*
* @param wait
*/
public final void setPollingWait(int wait) {
pollingWaitTime = wait;
}
/**
*
* @return
*/
public int getScheduleRate() {
return pollingWaitTime;
}
private boolean isPollingSensor() {
return pollingWaitTime > 0;
}
@Override
public final void onMessage(final ObjectMessage message) {
if (!isRunning()) {
notifyError("Plugin \"" + getName() + "\" receives a command while is not running. Turn on the plugin first ");
return;
}
Object payload;
try {
payload = message.getObject();
if (payload instanceof Command) {
final Command command = (Command) payload;
LOG.info("Plugin \"{}\" receives command [{}] with parameters '{''{'{}'}''}'", new Object[]{this.getName(), command.getName(), command.getProperties()});
Protocol.ActuatorOnCommandRunnable action;
lastDestination = message.getJMSReplyTo();
action = new Protocol.ActuatorOnCommandRunnable(command,
message.getJMSReplyTo(),
message.getJMSCorrelationID());
Protocol.ActuatorPerforms task = new Protocol.ActuatorPerforms(getApi().getAuth().pluginBindRunnablePrivileges(this, action));
task.start();
} else {
if (payload instanceof EventTemplate) {
final EventTemplate event = (EventTemplate) payload;
Protocol.ActuatorOnEventRunnable r = new Protocol.ActuatorOnEventRunnable(event);
Protocol.ActuatorPerforms task = new Protocol.ActuatorPerforms(getApi().getAuth().pluginBindRunnablePrivileges(this, r));
task.start();
}
}
} catch (JMSException ex) {
LOG.error(ex.getLocalizedMessage());
}
}
/**
* Sends a command.
*
* @param command
* @return
*/
protected Command send(Command command) {
return getBusService().send(command);
}
/**
*
* @param command
*/
public void reply(Command command) {
// sends back the command
final String defaultCorrelationID = "-1";
getBusService().reply(command, lastDestination, defaultCorrelationID);
}
private class ActuatorPerforms extends Thread {
public ActuatorPerforms(Runnable target) {
super(target, "freedomotic-protocol-executor");
}
}
public class ActuatorOnEventRunnable implements Runnable {
private final EventTemplate event;
ActuatorOnEventRunnable(EventTemplate e) {
this.event = e;
}
@Override
public void run() {
try {
// a command is supposed executed if the plugin doesen't say the contrary
onEvent(event);
} catch (Exception ex) {
LOG.error(ex.getLocalizedMessage());
}
}
}
public class ActuatorOnCommandRunnable implements Runnable {
private final Command command;
private final Destination reply;
private final String correlationID;
ActuatorOnCommandRunnable(Command c, Destination reply, String correlationID) {
this.command = c;
this.reply = reply;
this.correlationID = correlationID;
}
@Override
public void run() {
try {
// a command is supposed executed if the plugin doesen't say the contrary
command.setExecuted(true);
onCommand(command);
} catch (IOException ex) {
LOG.error(ex.getLocalizedMessage());
command.setExecuted(false);
} catch (UnableToExecuteException ex) {
command.setExecuted(false);
LOG.info("Plugin \"" + getName() + "\" failed to execute command [" + command.getName() + "]: " + ex.getMessage());
}
// automatic-reply-to-command is used when the plugin executes the command in a
// separate thread. In this cases the onCommand() returns immediately (as execution is forked in a thread)
// and sometimes this is not the intended behavior. Take a look at the Delayer plugin configuration
// it has to call reply(...) explicitely
if ((getConfiguration().getBooleanProperty("automatic-reply-to-commands", true)) //default value is true
&& (command.getReplyTimeout() > 0)) {
getBusService().reply(command, reply, correlationID); //sends back the command marked as executed or not
}
}
}
private class SensorThread
extends Thread {
@Override
public void run() {
try {
if (isPollingSensor()) {
Thread thisThread = Thread.currentThread();
while (sensorThread == thisThread) {
try {
Thread.sleep(pollingWaitTime);
synchronized (this) {
while (!isRunning() && (sensorThread == thisThread)) {
wait();
}
}
} catch (InterruptedException e) {
// TODO do Log?
}
onRun();
}
} else {
if (isRunning()) {
onRun();
}
}
} catch (Exception e) {
notifyCriticalError(e.getMessage(), e);
}
}
}
}