/** * 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.exec.internal; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang.StringUtils; import org.openhab.binding.exec.ExecBindingProvider; import org.openhab.core.binding.BindingConfig; import org.openhab.core.items.Item; import org.openhab.core.library.types.StringType; import org.openhab.core.types.Command; import org.openhab.core.types.State; import org.openhab.core.types.TypeParser; import org.openhab.model.item.binding.AbstractGenericBindingProvider; import org.openhab.model.item.binding.BindingConfigParseException; /** * <p> * This class can parse information from the generic binding format and * provides Exec binding information from it. It registers as a * {@link ExecBindingProvider} service as well. * </p> * * <p> * Here are some examples for valid binding configuration strings: * <ul> * <li><code>{ exec="ON:ssh user@openhab.org touch ~/test.txt" }</code> - connect to openhab.org via ssh and issue the * command 'touch ~/test.txt'</li> * <li><code>{ exec="OFF:ssh teichsta@openhab.org shutdown -p now" }</code></li> * <li><code>{ exec="OFF:ssh teichsta@wlan-router ifconfig wlan0 down" }</code></li> * <li> * <code>{ exec="OFF:some command, ON:'some other command\, \'which is quite \' more \\complex\\ ', *:and a fallback" }</code> * </li> * <li> * <code>{ exec=">[1:open /path/to/my/mp3/gong.mp3] >[2:open /path/to/my/mp3/greeting.mp3] >[*:open /path/to/my/mp3/generic.mp3]" }</code> * </li> * <li> * <code>{ exec="<[curl -s http://weather.yahooapis.com/forecastrss?w=566473&u=c:60000:XSLT(demo_yahoo_weather.xsl)]" }</code> * <li> * <li><code>{ exec="<[/bin/sh@@-c@@uptime | awk '{ print $10 }':60000:REGEX((.*?))]" }</code></li> * </ul> * * @author Thomas.Eichstaedt-Engelen * @author Pauli Anttila * @since 0.6.0 */ public class ExecGenericBindingProvider extends AbstractGenericBindingProvider implements ExecBindingProvider { /** * Artificial command for the exec-in configuration (which has no command * part by definition). Because we use this artificial command we can reuse * the {@link ExecBindingConfig} for both in- and out-configuration. */ protected static final Command IN_BINDING_KEY = StringType.valueOf("IN_BINDING"); /** * Artificial command to identify that state changes should be taken into account */ protected static final Command CHANGED_COMMAND_KEY = StringType.valueOf("CHANGED"); protected static final Command WILDCARD_COMMAND_KEY = StringType.valueOf("*"); /** {@link Pattern} which matches a binding configuration part */ private static final Pattern BASE_CONFIG_PATTERN = Pattern.compile("(<|>)\\[(.*?)\\](\\s|$)"); /** {@link Pattern} which matches an In-Binding */ private static final Pattern IN_BINDING_PATTERN = Pattern.compile("(.*?)?:(?!//)(\\d*):(.*)"); /** {@link Pattern} which matches an Out-Binding */ private static final Pattern OUT_BINDING_PATTERN = Pattern.compile("(.*?):(.*)"); /** * {@inheritDoc} */ @Override public String getBindingType() { return "exec"; } /** * @{inheritDoc} */ @Override public void validateItemType(Item item, String bindingConfig) throws BindingConfigParseException { // we accept all types of items } /** * {@inheritDoc} */ @Override public void processBindingConfiguration(String context, Item item, String bindingConfig) throws BindingConfigParseException { super.processBindingConfiguration(context, item, bindingConfig); ExecBindingConfig config = new ExecBindingConfig(); config.acceptedDataTypes = new ArrayList<Class<? extends State>>(item.getAcceptedDataTypes()); Matcher matcher = BASE_CONFIG_PATTERN.matcher(bindingConfig); if (!matcher.matches()) { if (bindingConfig.startsWith("<") || bindingConfig.startsWith(">")) { throw new BindingConfigParseException("Exec binding legacy format cannot start with '<' or '>' "); } // backward compatibility for old format parseLegacyOutBindingConfig(item, bindingConfig, config); } else { matcher.reset(); while (matcher.find()) { String direction = matcher.group(1); String bindingConfigPart = matcher.group(2); if (direction.equals("<")) { config = parseInBindingConfig(item, bindingConfigPart, config); } else if (direction.equals(">")) { config = parseOutBindingConfig(item, bindingConfigPart, config); } else { throw new BindingConfigParseException( "Unknown command given! Configuration must start with '<' or '>' "); } } } addBindingConfig(item, config); } protected ExecBindingConfig parseInBindingConfig(Item item, String bindingConfig, ExecBindingConfig config) throws BindingConfigParseException { Matcher matcher = IN_BINDING_PATTERN.matcher(bindingConfig); if (!matcher.matches()) { throw new BindingConfigParseException( "bindingConfig '" + bindingConfig + "' doesn't represent a valid in-binding-configuration."); } matcher.reset(); ExecBindingConfigElement configElement; while (matcher.find()) { configElement = new ExecBindingConfigElement(); configElement.commandLine = matcher.group(1).replaceAll("\\\\\"", ""); configElement.refreshInterval = Integer.valueOf(matcher.group(2)).intValue(); configElement.transformation = matcher.group(3).replaceAll("\\\\\"", "\""); config.put(IN_BINDING_KEY, configElement); } return config; } protected ExecBindingConfig parseOutBindingConfig(Item item, String bindingConfig, ExecBindingConfig config) throws BindingConfigParseException { Matcher matcher = OUT_BINDING_PATTERN.matcher(bindingConfig); if (!matcher.matches()) { throw new BindingConfigParseException( "bindingConfig '" + bindingConfig + "' doesn't represent a valid in-binding-configuration."); } matcher.reset(); ExecBindingConfigElement configElement; while (matcher.find()) { Command command = createCommandFromString(item, matcher.group(1)); configElement = new ExecBindingConfigElement(); configElement.commandLine = matcher.group(2).replaceAll("\\\\\"", ""); config.put(command, configElement); } return config; } protected void parseLegacyOutBindingConfig(Item item, String bindingConfig, ExecBindingConfig config) throws BindingConfigParseException { String command = StringUtils.substringBefore(bindingConfig, ":").trim(); String tmpCommandLine = StringUtils.substringAfter(bindingConfig, ":").trim(); if (StringUtils.isBlank(command) && StringUtils.isBlank(tmpCommandLine)) { return; } String commandLine; // if commandLine is surrounded by quotes, life is easy ... if (tmpCommandLine.startsWith("'")) { commandLine = tmpCommandLine.substring(1).split("(?<!\\\\)'")[0]; // is there another command we have to parse? String tail = tmpCommandLine.replaceFirst(".*(?<!\\\\)' ?,", "").trim(); if (!tail.isEmpty()) { parseLegacyOutBindingConfig(item, tail, config); } } else { // if not, we have to search for the next "," (if there are more than // one commandLines) or for the end of this line. String[] tmpCommandLineElements = tmpCommandLine.split("(?<!\\\\),"); if (tmpCommandLineElements.length == 0) { commandLine = tmpCommandLine; } else { commandLine = tmpCommandLineElements[0]; String tail = StringUtils.join(tmpCommandLineElements, ", ", 1, tmpCommandLineElements.length); parseLegacyOutBindingConfig(item, tail, config); } } ExecBindingConfigElement configElement = new ExecBindingConfigElement(); configElement.commandLine = commandLine.replaceAll("(?<!\\\\)\\\\", ""); Command cmd = createCommandFromString(item, command); config.put(cmd, configElement); } /** * Creates a {@link Command} out of the given <code>commandAsString</code> * taking the special Commands "CHANGED" and "*" into account and incorporating * the {@link TypeParser}. * * @param item * @param commandAsString * * @return an appropriate Command (see {@link TypeParser} for more * information * * @throws BindingConfigParseException if the {@link TypeParser} couldn't * create a command appropriately * * @see {@link TypeParser} */ private Command createCommandFromString(Item item, String commandAsString) throws BindingConfigParseException { if (CHANGED_COMMAND_KEY.equals(commandAsString)) { return CHANGED_COMMAND_KEY; } else if (WILDCARD_COMMAND_KEY.equals(commandAsString)) { return WILDCARD_COMMAND_KEY; } else { Command command = TypeParser.parseCommand(item.getAcceptedCommandTypes(), commandAsString); if (command == null) { throw new BindingConfigParseException("couldn't create Command from '" + commandAsString + "' "); } return command; } } /** * @{inheritDoc} */ @Override public List<Class<? extends State>> getAcceptedDataTypes(String itemName) { ExecBindingConfig config = (ExecBindingConfig) bindingConfigs.get(itemName); return config != null ? config.acceptedDataTypes : null; } /** * {@inheritDoc} */ @Override public String getCommandLine(String itemName, Command command) { try { ExecBindingConfig config = (ExecBindingConfig) bindingConfigs.get(itemName); return config != null ? config.get(command).commandLine : null; } catch (NullPointerException e) { return null; } } /** * {@inheritDoc} */ @Override public String getCommandLine(String itemName) { ExecBindingConfig config = (ExecBindingConfig) bindingConfigs.get(itemName); return config != null && config.get(IN_BINDING_KEY) != null ? config.get(IN_BINDING_KEY).commandLine : null; } /** * {@inheritDoc} */ @Override public int getRefreshInterval(String itemName) { ExecBindingConfig config = (ExecBindingConfig) bindingConfigs.get(itemName); return config != null && config.get(IN_BINDING_KEY) != null ? config.get(IN_BINDING_KEY).refreshInterval : 0; } /** * {@inheritDoc} */ @Override public String getTransformation(String itemName) { ExecBindingConfig config = (ExecBindingConfig) bindingConfigs.get(itemName); return config != null && config.get(IN_BINDING_KEY) != null ? config.get(IN_BINDING_KEY).transformation : null; } /** * {@inheritDoc} */ @Override public List<String> getInBindingItemNames() { List<String> inBindings = new ArrayList<String>(); for (String itemName : bindingConfigs.keySet()) { ExecBindingConfig config = (ExecBindingConfig) bindingConfigs.get(itemName); if (config.containsKey(IN_BINDING_KEY)) { inBindings.add(itemName); } } return inBindings; } /** * This is an internal data structure to store information from the binding * config strings and use it to answer the requests to the Exec * binding provider. */ static class ExecBindingConfig extends HashMap<Command, ExecBindingConfigElement>implements BindingConfig { /** generated serialVersion UID */ private static final long serialVersionUID = 6164971643530954095L; List<Class<? extends State>> acceptedDataTypes; } /** * This is an internal data structure to store information from the binding * config strings and use it to answer the requests to the Exec binding * provider. */ static class ExecBindingConfigElement implements BindingConfig { public String commandLine = null; int refreshInterval = 0; String transformation = null; @Override public String toString() { return "ExecBindingConfigElement [command=" + commandLine + ", refreshInterval=" + refreshInterval + ", transformation=" + transformation + "]"; } } }