/** * 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.tcp.protocol.internal; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collection; 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.tcp.Direction; import org.openhab.binding.tcp.protocol.ProtocolBindingProvider; import org.openhab.core.binding.BindingConfig; import org.openhab.core.items.Item; import org.openhab.core.library.items.NumberItem; import org.openhab.core.library.types.DecimalType; 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; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * * The "standard" TCP and UDP network bindings follow the same configuration format: * * tcp=">[ON:192.168.0.1:3000:'some text'], >[OFF:192.168.0.1:3000:'some other command']" * tcp="<[192.168.0.1:3000:'some text']" - for String Items * * direction[openhab command:hostname:port number:protocol command] * * For String Items, the Item will be updated with the incoming string * openhab commands can be repeated more than once for a given Item, e.g. receiving ON command could trigger to pieces * of data to be sent to for example to different host:port combinations,... * * @author Karel Goderis * @since 1.1.0 */ abstract class ProtocolGenericBindingProvider extends AbstractGenericBindingProvider implements ProtocolBindingProvider { static final Logger logger = LoggerFactory.getLogger(ProtocolGenericBindingProvider.class); /** {@link Pattern} which matches a binding configuration part */ private static final Pattern BASE_CONFIG_PATTERN = Pattern.compile("([<>]\\[.*?\\])(?:[,\\s]|$)"); private static final Pattern ACTION_CONFIG_PATTERN = Pattern .compile("(<|>)\\[(.*?):(.*?):(.*?):(?:'(.*)'|(.*))\\]"); private static final Pattern STATUS_CONFIG_PATTERN = Pattern.compile("(<|>)\\[(.*?):(.*?):(?:'(.*)'|(.*))\\]"); private static final Command WILDCARD_COMMAND_KEY = StringType.valueOf("*"); static int counter = 0; @Override public void validateItemType(Item item, String bindingConfig) throws BindingConfigParseException { // All Item Types are accepted by ProtocolGenericBindingProvider } /** * {@inheritDoc} */ @Override public void processBindingConfiguration(String context, Item item, String bindingConfig) throws BindingConfigParseException { super.processBindingConfiguration(context, item, bindingConfig); if (bindingConfig != null) { parseAndAddBindingConfig(item, bindingConfig); } else { logger.warn(getBindingType() + " bindingConfig is NULL (item=" + item + ") -> processing bindingConfig aborted!"); } } private void parseAndAddBindingConfig(Item item, String bindingConfig) throws BindingConfigParseException { ProtocolBindingConfig newConfig = new ProtocolBindingConfig(); Matcher matcher = BASE_CONFIG_PATTERN.matcher(bindingConfig); if (!matcher.matches()) { throw new BindingConfigParseException( "bindingConfig '" + bindingConfig + "' doesn't contain a valid binding configuration"); } matcher.reset(); while (matcher.find()) { String bindingConfigPart = matcher.group(1); if (StringUtils.isNotBlank(bindingConfigPart)) { parseBindingConfig(newConfig, item, bindingConfigPart); addBindingConfig(item, newConfig); } } } /** * Parses the configuration string and update the provided config * * @param config - the Configuration that needs to be updated with the parsing results * @param item - the Item that this configuration is intented for * @param bindingConfig - the configuration string that will be parsed * @throws BindingConfigParseException */ private void parseBindingConfig(ProtocolBindingConfig config, Item item, String bindingConfig) throws BindingConfigParseException { String direction = null; Direction directionType = null; String commandAsString = null; String host = null; String port = null; String protocolCommand = null; if (bindingConfig != null) { Matcher actionMatcher = ACTION_CONFIG_PATTERN.matcher(bindingConfig); Matcher statusMatcher = STATUS_CONFIG_PATTERN.matcher(bindingConfig); if ((!actionMatcher.matches() && !statusMatcher.matches())) { throw new BindingConfigParseException( getBindingType() + " binding configuration must consist of four [config=" + statusMatcher + "] or five parts [config=" + actionMatcher + "]"); } else { if (actionMatcher.matches()) { direction = actionMatcher.group(1); commandAsString = actionMatcher.group(2); host = actionMatcher.group(3); port = actionMatcher.group(4); protocolCommand = actionMatcher.group(5) != null ? actionMatcher.group(5) : actionMatcher.group(6); } else if (statusMatcher.matches()) { direction = statusMatcher.group(1); commandAsString = null; host = statusMatcher.group(2); port = statusMatcher.group(3); protocolCommand = statusMatcher.group(4) != null ? statusMatcher.group(4) : statusMatcher.group(5); } if (direction.equals(">")) { directionType = Direction.OUT; } else if (direction.equals("<")) { directionType = Direction.IN; } ProtocolBindingConfigElement newElement = new ProtocolBindingConfigElement(host, port, directionType, protocolCommand, item.getAcceptedDataTypes()); Command command = null; if (commandAsString == null) { // for those configuration strings that are not really linked to a openHAB command we // create a dummy Command to be able to store the configuration information // I have choosen to do that with NumberItems NumberItem dummy = new NumberItem(Integer.toString(counter)); command = createCommandFromString(dummy, Integer.toString(counter)); counter++; } else { command = createCommandFromString(item, commandAsString); } config.put(command, newElement); } } else { return; } } /** * Creates a {@link Command} out of the given <code>commandAsString</code> * 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 (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 String getHost(String itemName, Command command) { ProtocolBindingConfig config = (ProtocolBindingConfig) bindingConfigs.get(itemName); return config != null && config.get(command) != null ? config.get(command).getHost() : null; } /** * {@inheritDoc} */ @Override public int getPort(String itemName, Command command) { ProtocolBindingConfig config = (ProtocolBindingConfig) bindingConfigs.get(itemName); return config != null && config.get(command) != null ? Integer.parseInt(config.get(command).getPort()) : null; } /** * {@inheritDoc} */ @Override public String getPortAsString(String itemName, Command command) { ProtocolBindingConfig config = (ProtocolBindingConfig) bindingConfigs.get(itemName); return config != null && config.get(command) != null ? config.get(command).getPort() : null; } /** * {@inheritDoc} */ @Override public Direction getDirection(String itemName, Command command) { ProtocolBindingConfig config = (ProtocolBindingConfig) bindingConfigs.get(itemName); return config != null && config.get(command) != null ? config.get(command).getDirection() : null; } /** * {@inheritDoc} */ @Override public String getProtocolCommand(String itemName, Command command) { ProtocolBindingConfig config = (ProtocolBindingConfig) bindingConfigs.get(itemName); return config != null && config.get(command) != null ? config.get(command).getNetworkCommand() : null; } /** * {@inheritDoc} */ @Override public List<Command> getQualifiedCommands(String itemName, Command command) { List<Command> commands = new ArrayList<Command>(); ProtocolBindingConfig aConfig = (ProtocolBindingConfig) bindingConfigs.get(itemName); for (Command aCommand : aConfig.keySet()) { if (aCommand.equals(command) || aCommand.equals(WILDCARD_COMMAND_KEY) || aCommand instanceof DecimalType) { commands.add(aCommand); } } return commands; } /** * This is an internal data structure to map commands to * {@link ProtocolBindingConfigElement }. There will be map like * <code>ON->ProtocolBindingConfigElement</code> */ static class ProtocolBindingConfig extends HashMap<Command, ProtocolBindingConfigElement> implements BindingConfig { private static final long serialVersionUID = 6363085986521089771L; } static class ProtocolBindingConfigElement implements BindingConfig { final private String host; final private String port; final private Direction direction; final private String networkCommand; final private List<Class<? extends State>> acceptedTypes; public ProtocolBindingConfigElement(String host, String port, Direction direction, String networkCommand, List<Class<? extends State>> acceptedTypes) { this.host = host; this.port = port; this.direction = direction; this.networkCommand = networkCommand; this.acceptedTypes = acceptedTypes; } @Override public String toString() { return "ProtocolBindingConfigElement [Direction=" + direction + ", host=" + host + ", port=" + port + ", cmd=" + networkCommand + "]"; } /** * @return the networkCommand */ public String getNetworkCommand() { return networkCommand; } /** * @return the direction */ public Direction getDirection() { return direction; } /** * @return the host */ public String getHost() { return host; } /** * @return the port */ public String getPort() { return port; } /** * @return the list of accepted DataTypes for the Item linked to this Binding Config Element */ public List<Class<? extends State>> getAcceptedTypes() { return acceptedTypes; } } @Override public InetSocketAddress getInetSocketAddress(String itemName, Command command) { ProtocolBindingConfig config = (ProtocolBindingConfig) bindingConfigs.get(itemName); ProtocolBindingConfigElement element = config.get(command); InetSocketAddress socketAddress = new InetSocketAddress(element.getHost(), Integer.parseInt(element.getPort())); return socketAddress; } @Override public List<InetSocketAddress> getInetSocketAddresses(String itemName) { List<InetSocketAddress> theList = new ArrayList<InetSocketAddress>(); ProtocolBindingConfig config = (ProtocolBindingConfig) bindingConfigs.get(itemName); if (config != null) { for (Command command : config.keySet()) { InetSocketAddress anAddress = null; try { anAddress = new InetSocketAddress(InetAddress.getByName(config.get(command).getHost()), Integer.parseInt(config.get(command).getPort())); } catch (UnknownHostException e) { logger.warn("Could not resolve the hostname {} for item {}", config.get(command).getHost(), itemName); } theList.add(anAddress); } } return theList; } @Override public Collection<String> getItemNames(String host, int port) { List<String> items = new ArrayList<String>(); for (String itemName : bindingConfigs.keySet()) { ProtocolBindingConfig aConfig = (ProtocolBindingConfig) bindingConfigs.get(itemName); for (Command aCommand : aConfig.keySet()) { ProtocolBindingConfigElement anElement = aConfig.get(aCommand); if (anElement.getHost().equals(host) && anElement.getPort() == Integer.toString(port)) { if (!items.contains(itemName)) { items.add(itemName); } } } } return items; } @Override public List<String> getItemNames(String protocolCommand) { List<String> itemNames = new ArrayList<String>(); for (String itemName : bindingConfigs.keySet()) { ProtocolBindingConfig aConfig = (ProtocolBindingConfig) bindingConfigs.get(itemName); for (Command aCommand : aConfig.keySet()) { ProtocolBindingConfigElement anElement = aConfig.get(aCommand); if (anElement.networkCommand.equals(protocolCommand)) { itemNames.add(itemName); } } } return itemNames; } @Override public List<Command> getAllCommands(String itemName, String protocolCommand) { List<Command> commands = new ArrayList<Command>(); ProtocolBindingConfig aConfig = (ProtocolBindingConfig) bindingConfigs.get(itemName); if (aConfig != null) { for (Command aCommand : aConfig.keySet()) { ProtocolBindingConfigElement anElement = aConfig.get(aCommand); if (anElement.networkCommand.equals(protocolCommand)) { commands.add(aCommand); } } } return commands; } @Override public List<Command> getAllCommands(String itemName) { List<Command> commands = new ArrayList<Command>(); ProtocolBindingConfig aConfig = (ProtocolBindingConfig) bindingConfigs.get(itemName); if (aConfig != null) { for (Command aCommand : aConfig.keySet()) { commands.add(aCommand); } } return commands; } @Override public List<Class<? extends State>> getAcceptedDataTypes(String itemName, Command command) { if (itemName != null) { ProtocolBindingConfig config = (ProtocolBindingConfig) bindingConfigs.get(itemName); if (config != null) { ProtocolBindingConfigElement element = config.get(command); if (element != null) { return element.getAcceptedTypes(); } } } return null; } }