/**
* Copyright (c) 2010-2014, openHAB.org and others.
*
* 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.exec.internal;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecuteResultHandler;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteException;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.exec.Executor;
import org.apache.commons.exec.PumpStreamHandler;
import org.apache.commons.lang.StringUtils;
import org.openhab.binding.exec.ExecBindingProvider;
import org.openhab.core.binding.AbstractActiveBinding;
import org.openhab.core.items.Item;
import org.openhab.core.library.items.ContactItem;
import org.openhab.core.library.items.NumberItem;
import org.openhab.core.library.items.RollershutterItem;
import org.openhab.core.library.items.SwitchItem;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.transform.TransformationException;
import org.openhab.core.transform.TransformationHelper;
import org.openhab.core.transform.TransformationService;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.TypeParser;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
//import sun.tools.jar.CommandLine;
/**
* The swiss army knife binding which executes given commands on the commandline.
* It could act as the opposite of WoL and sends the shutdown command to servers.
* Or switches of WLAN connectivity if a scene "sleeping" is activated.
* <p>
* <i>Note</i>: when using 'ssh' you should use private key authorization since
* the password cannot be read from commandline. The given user should have the
* necessary permissions.
*
* @author Thomas.Eichstaedt-Engelen
* @author Pauli Anttila
* @since 0.6.0
*/
public class ExecBinding extends AbstractActiveBinding<ExecBindingProvider> implements ManagedService {
private static final Logger logger = LoggerFactory.getLogger(ExecBinding.class);
protected static final Command WILDCARD_COMMAND_KEY = StringType.valueOf("*");
private static final String CMD_LINE_DELIMITER = "@@";
/** the timeout for executing command (defaults to 60000 milliseconds) */
private int timeout = 60000;
/** the interval to find new refresh candidates (defaults to 1000 milliseconds)*/
private int granularity = 1000;
private Map<String, Long> lastUpdateMap = new HashMap<String, Long>();
/** RegEx to extract a parse a function String <code>'(.*?)\((.*)\)'</code> */
private static final Pattern EXTRACT_FUNCTION_PATTERN = Pattern.compile("(.*?)\\((.*)\\)");
@Override
protected long getRefreshInterval() {
return granularity;
}
@Override
protected String getName() {
return "Exec Refresh Service";
}
@Override
public void activate() {
super.activate();
setProperlyConfigured(true);
}
public void execute() {
for (ExecBindingProvider provider : providers) {
for (String itemName : provider.getInBindingItemNames()) {
String commandLine = provider.getCommandLine(itemName);
int refreshInterval = provider.getRefreshInterval(itemName);
String transformation = provider.getTransformation(itemName);
Long lastUpdateTimeStamp = lastUpdateMap.get(itemName);
if (lastUpdateTimeStamp == null) {
lastUpdateTimeStamp = 0L;
}
long age = System.currentTimeMillis() - lastUpdateTimeStamp;
boolean needsUpdate = age >= refreshInterval;
if (needsUpdate) {
logger.debug("item '{}' is about to be refreshed now", itemName);
commandLine = String.format(commandLine, Calendar.getInstance().getTime(), "", itemName);
String response = executeCommandAndWaitResponse(commandLine);
if(response==null) {
logger.error("No response received from command '{}'", commandLine);
} else {
String transformedResponse;
try {
String[] parts = splitTransformationConfig(transformation);
String transformationType = parts[0];
String transformationFunction = parts[1];
TransformationService transformationService =
TransformationHelper.getTransformationService(ExecActivator.getContext(), transformationType);
if (transformationService != null) {
transformedResponse = transformationService.transform(transformationFunction, response);
} else {
transformedResponse = response;
logger.warn("couldn't transform response because transformationService of type '{}' is unavailable", transformationType);
}
}
catch (TransformationException te) {
logger.error("transformation throws exception [transformation="
+ transformation + ", response=" + response + "]", te);
// in case of an error we return the response without any
// transformation
transformedResponse = response;
}
logger.debug("transformed response is '{}'", transformedResponse);
Class<? extends Item> itemType = provider.getItemType(itemName);
State state = createState(itemType, transformedResponse);
if (state != null) {
eventPublisher.postUpdate(itemName, state);
}
}
lastUpdateMap.put(itemName, System.currentTimeMillis());
}
}
}
}
/**
* Splits a transformation configuration string into its two parts - the
* transformation type and the function/pattern to apply.
*
* @param transformation the string to split
* @return a string array with exactly two entries for the type and the function
*/
protected String[] splitTransformationConfig(String transformation) {
Matcher matcher = EXTRACT_FUNCTION_PATTERN.matcher(transformation);
if (!matcher.matches()) {
throw new IllegalArgumentException("given transformation function '" + transformation + "' does not follow the expected pattern '<function>(<pattern>)'");
}
matcher.reset();
matcher.find();
String type = matcher.group(1);
String pattern = matcher.group(2);
return new String[] { type, pattern };
}
/**
* Returns a {@link State} which is inherited from the {@link Item}s
* accepted DataTypes. The call is delegated to the {@link TypeParser}. If
* <code>item</code> is <code>null</code> the {@link StringType} is used.
*
* @param itemType
* @param transformedResponse
*
* @return a {@link State} which type is inherited by the {@link TypeParser}
* or a {@link StringType} if <code>item</code> is <code>null</code>
*/
private State createState(Class<? extends Item> itemType, String transformedResponse) {
try {
if (itemType.isAssignableFrom(NumberItem.class)) {
return DecimalType.valueOf(transformedResponse);
} else if (itemType.isAssignableFrom(ContactItem.class)) {
return OpenClosedType.valueOf(transformedResponse);
} else if (itemType.isAssignableFrom(SwitchItem.class)) {
return OnOffType.valueOf(transformedResponse);
} else if (itemType.isAssignableFrom(RollershutterItem.class)) {
return PercentType.valueOf(transformedResponse);
} else {
return StringType.valueOf(transformedResponse);
}
} catch (Exception e) {
logger.debug("Couldn't create state of type '{}' for value '{}'", itemType, transformedResponse);
return StringType.valueOf(transformedResponse);
}
}
/**
* @{inheritDoc}
*/
@Override
public void internalReceiveCommand(String itemName, Command command) {
ExecBindingProvider provider =
findFirstMatchingBindingProvider(itemName, command);
if (provider == null) {
logger.warn("doesn't find matching binding provider [itemName={}, command={}]", itemName, command);
return;
}
String commandLine = provider.getCommandLine(itemName, command);
// fallback
if (commandLine == null) {
commandLine = provider.getCommandLine(itemName, WILDCARD_COMMAND_KEY);
}
if (commandLine != null && !commandLine.isEmpty()) {
commandLine = String.format(commandLine, Calendar.getInstance().getTime(), command, itemName);
executeCommand(commandLine);
}
}
/**
* Find the first matching {@link ExecBindingProvider} according to
* <code>itemName</code> and <code>command</code>. If no direct match is
* found, a second match is issued with wilcard-command '*'.
*
* @param itemName
* @param command
*
* @return the matching binding provider or <code>null</code> if no binding
* provider could be found
*/
private ExecBindingProvider findFirstMatchingBindingProvider(String itemName, Command command) {
ExecBindingProvider firstMatchingProvider = null;
for (ExecBindingProvider provider : this.providers) {
String commandLine = provider.getCommandLine(itemName, command);
if (commandLine != null) {
firstMatchingProvider = provider;
break;
}
}
// we didn't find an exact match. probably one configured a fallback
// command?
if (firstMatchingProvider == null) {
for (ExecBindingProvider provider : this.providers) {
String commandLine = provider.getCommandLine(itemName, WILDCARD_COMMAND_KEY);
if (commandLine != null) {
firstMatchingProvider = provider;
break;
}
}
}
return firstMatchingProvider;
}
/**
* <p>Executes <code>commandLine</code>. Sometimes (especially observed on
* MacOS) the commandLine isn't executed properly. In that cases another
* exec-method is to be used. To accomplish this please use the special
* delimiter '<code>@@</code>'. If <code>commandLine</code> contains this
* delimiter it is split into a String[] array and the special exec-method
* is used.</p>
* <p>A possible {@link IOException} gets logged but no further processing is
* done.</p>
*
* @param commandLine the command line to execute
* @see http://www.peterfriese.de/running-applescript-from-java/
*/
private void executeCommand(String commandLine) {
try {
if (commandLine.contains(CMD_LINE_DELIMITER)) {
String[] cmdArray = commandLine.split(CMD_LINE_DELIMITER);
Runtime.getRuntime().exec(cmdArray);
logger.info("executed commandLine '{}'", Arrays.asList(cmdArray));
} else {
Runtime.getRuntime().exec(commandLine);
logger.info("executed commandLine '{}'", commandLine);
}
}
catch (IOException e) {
logger.error("couldn't execute commandLine '" + commandLine + "'", e);
}
}
/**
* <p>Executes <code>commandLine</code>. Sometimes (especially observed on
* MacOS) the commandLine isn't executed properly. In that cases another
* exec-method is to be used. To accomplish this please use the special
* delimiter '<code>@@</code>'. If <code>commandLine</code> contains this
* delimiter it is split into a String[] array and the special exec-method
* is used.</p>
* <p>A possible {@link IOException} gets logged but no further processing is
* done.</p>
*
* @param commandLine the command line to execute
* @return response data from executed command line
*/
private String executeCommandAndWaitResponse(String commandLine) {
String retval = null;
CommandLine cmdLine = null;
if (commandLine.contains(CMD_LINE_DELIMITER)) {
String[] cmdArray = commandLine.split(CMD_LINE_DELIMITER);
cmdLine = new CommandLine(cmdArray[0]);
for (int i = 1; i < cmdArray.length; i++) {
cmdLine.addArgument(cmdArray[i], false);
}
} else {
cmdLine = CommandLine.parse(commandLine);
}
DefaultExecuteResultHandler resultHandler = new DefaultExecuteResultHandler();
ExecuteWatchdog watchdog = new ExecuteWatchdog(timeout);
Executor executor = new DefaultExecutor();
ByteArrayOutputStream stdout = new ByteArrayOutputStream();
PumpStreamHandler streamHandler = new PumpStreamHandler(stdout);
executor.setExitValue(1);
executor.setStreamHandler(streamHandler);
executor.setWatchdog(watchdog);
try {
executor.execute(cmdLine, resultHandler);
logger.debug("executed commandLine '{}'", commandLine);
} catch (ExecuteException e) {
logger.error("couldn't execute commandLine '" + commandLine + "'", e);
} catch (IOException e) {
logger.error("couldn't execute commandLine '" + commandLine + "'", e);
}
// some time later the result handler callback was invoked so we
// can safely request the exit code
try {
resultHandler.waitFor();
int exitCode = resultHandler.getExitValue();
retval = StringUtils.chomp(stdout.toString());
logger.debug("exit code '{}', result '{}'", exitCode, retval);
} catch (InterruptedException e) {
logger.error("Timeout occured when executing commandLine '" + commandLine + "'", e);
}
return retval;
}
@Override
@SuppressWarnings("rawtypes")
public void updated(Dictionary config) throws ConfigurationException {
if (config != null) {
String timeoutString = (String) config.get("timeout");
if (StringUtils.isNotBlank(timeoutString)) {
timeout = Integer.parseInt(timeoutString);
}
String granularityString = (String) config.get("granularity");
if (StringUtils.isNotBlank(granularityString)) {
granularity = Integer.parseInt(granularityString);
}
}
}
@Override
public void addBindingProvider(ExecBindingProvider provider) {
super.addBindingProvider(provider);
setProperlyConfigured(true);
}
}