/** * 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.http.internal; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Properties; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.openhab.binding.http.HttpBindingProvider; 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; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * <p> * This class can parse information from the generic binding format and * provides HTTP binding information from it. It registers as an * {@link HttpBindingProvider} service as well. * </p> * * <p> * Here are some examples for valid binding configuration strings: * <ul> * <li> * <code>{ http=">[ON:POST:http://www.domain.org/home/lights/23871?status=on] >[OFF:POST:http://www.domain.org/home/lights/23871?status=off]" }</code> * </li> * <li><code>{ http="<[http://www.domain.org/weather/openhabcity/daily:60000:REGEX(.*)]" }</code></li> * <li> * <code>{ http=">[ON:POST:http://www.domain.org/home/lights/23871?status=on] >[OFF:POST:http://www.domain.org/home/lights/23871?status=off] <[http://www.domain.org/weather/openhabcity/daily:60000:REGEX(.*)]" }</code> * </li> * <li><code>{ http=">[*:POST:http://www.domain.org/home/lights/23871?status=%2$s&date=%1$tY-%1$tm-%1$td]" }</code></li> * <li> * <code>{ http=">[CHANGED:POST:http://www.domain.org/home/lights/23871?status=%2$s&date=%1$tY-%1$tm-%1$td]" }</code> * </li> * <li> * <code>{ http=">[CHANGED:POST:http://www.domain.org/home/lights/23871?status=%2$s&date=%1$tY-%1$tm-%1$td{AuthKey=somekey&timerange=day}]" }</code> * </li> * <li> * <code>{ http="<[https://www.flukso.net/api/sensor/xxxx?interval=daily{X-Token=mytoken&X-version=1.0}:60000:REGEX(.*?<title>(.*?)</title>(.*))]" }</code> * </li> * </ul> * * @author Thomas.Eichstaedt-Engelen * @author Chris Carman * * @since 0.6.0 */ public class HttpGenericBindingProvider extends AbstractGenericBindingProvider implements HttpBindingProvider { static final Logger logger = LoggerFactory.getLogger(HttpGenericBindingProvider.class); /** * Artificial command for the http-in configuration (which has no command * part by definition). Because we use this artificial command we can reuse * the {@link HttpBindingConfig} 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("(.*?):([A-Z]*):(.*)"); /** {@link Pattern} that separates a url string from the following post body string */ private static final Pattern URL_PARSING_PATTERN = Pattern .compile("^((([^:/?#]+):)?(//([^/?#]*))?([^?#:]*)(\\?([^#:]*))?(#(.*))?)(:.*)?"); /** * {@inheritDoc} */ @Override public String getBindingType() { return "http"; } /** * @{inheritDoc} */ @Override public void validateItemType(Item item, String bindingConfig) throws BindingConfigParseException { } /** * {@inheritDoc} */ @Override public void processBindingConfiguration(String context, Item item, String bindingConfig) throws BindingConfigParseException { super.processBindingConfiguration(context, item, bindingConfig); if (bindingConfig != null) { HttpBindingConfig config = parseBindingConfig(item, bindingConfig); addBindingConfig(item, config); } else { logger.warn("bindingConfig is NULL (item=" + item + ") -> process bindingConfig aborted!"); } } /** * Delegates parsing the <code>bindingConfig</code> with respect to the * first character (<code><</code> or <code>></code>) to the * specialized parsing methods * * @param item * @param bindingConfig * * @throws BindingConfigParseException */ protected HttpBindingConfig parseBindingConfig(Item item, String bindingConfig) throws BindingConfigParseException { HttpBindingConfig config = new HttpBindingConfig(item); 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 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 '>' "); } } return config; } /** * Parses a http-in configuration by using the regular expression * <code>(.*?):(\\d*):(.*)</code>. Where the groups should contain the * following content: * <ul> * <li>1 - url</li> * <li>2 - refresh interval</li> * <li>3 - the transformation rule</li> * </ul> * * @param item * * @param bindingConfig the config string to parse * @param config * @return the filled {@link HttpBindingConfig} * * @throws BindingConfigParseException if the regular expression doesn't match * the given <code>bindingConfig</code> */ protected HttpBindingConfig parseInBindingConfig(Item item, String bindingConfig, HttpBindingConfig 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. A valid configuration is matched by the RegExp '" + IN_BINDING_PATTERN + "'"); } matcher.reset(); HttpBindingConfigElement configElement; while (matcher.find()) { configElement = new HttpBindingConfigElement(); configElement.url = matcher.group(1).replaceAll("\\\\\"", ""); configElement.headers = parseHttpHeaders(matcher.group(2)); configElement.refreshInterval = Integer.valueOf(matcher.group(3)).intValue(); configElement.transformation = matcher.group(4).replaceAll("\\\\\"", "\""); config.put(IN_BINDING_KEY, configElement); } return config; } private Properties parseHttpHeaders(String group) { Properties headers = new Properties(); if (group != null && group.length() > 0) { if (group.startsWith("{")) { group = group.substring(1); } if (group.endsWith("}")) { group = group.substring(0, group.length() - 1); } String[] headersArray = group.split("&"); for (String headerElement : headersArray) { int idx = headerElement.indexOf("="); if (idx >= 0) { headers.setProperty(headerElement.substring(0, idx), headerElement.substring(idx + 1)); } } } return headers; } /** * Parses an http-out configuration by using the regular expression * <code>(.*?):([A-Z]*):(.*)</code>. Where the groups should contain the * following content: * <ul> * <li>1 - command</li> * <li>2 - http method</li> * <li>3 - url</li> * <li>4 - post body</li> * </ul> * * @param item * @param bindingConfig the config string to parse * @param config * @return the filled {@link HttpBindingConfig} * * @throws BindingConfigParseException if the regular expression doesn't match * the given <code>bindingConfig</code> */ protected HttpBindingConfig parseOutBindingConfig(Item item, String bindingConfig, HttpBindingConfig config) throws BindingConfigParseException { logger.debug("parsing this as an http out binding: {}", bindingConfig); Matcher matcher = OUT_BINDING_PATTERN.matcher(bindingConfig); if (!matcher.matches()) { throw new BindingConfigParseException("bindingConfig '" + bindingConfig + "' doesn't contain a valid out-binding-configuration. A valid configuration is matched by the RegExp '" + OUT_BINDING_PATTERN + "'"); } matcher.reset(); HttpBindingConfigElement configElement; while (matcher.find()) { configElement = new HttpBindingConfigElement(); Command command = createCommandFromString(item, matcher.group(1)); configElement.httpMethod = matcher.group(2); String lastPart = matcher.group(3).replaceAll("\\\\\"", ""); logger.debug("URL portion of binding config to be processed: {}", lastPart); Matcher urlMatcher = URL_PARSING_PATTERN.matcher(lastPart); urlMatcher.find(); if (logger.isDebugEnabled()) { for (int i = 0; i <= urlMatcher.groupCount(); i++) { logger.debug("Group {}: {}", i, urlMatcher.group(i)); } } if (urlMatcher.group(1).endsWith("}")) { String g1 = urlMatcher.group(1); int beginIdx = g1.indexOf("{"); int endIdx = g1.indexOf("}"); configElement.url = g1.substring(0, beginIdx); configElement.headers = parseHttpHeaders(g1.substring(beginIdx + 1, endIdx)); } else { configElement.url = urlMatcher.group(1); } if (configElement.httpMethod.equals("POST") && urlMatcher.group(11) != null) { configElement.body = urlMatcher.group(11).substring(1); } config.put(command, configElement); } return config; } /** * 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 State getState(String itemName, String value) { HttpBindingConfig config = (HttpBindingConfig) bindingConfigs.get(itemName); if (config != null) { List<Class<? extends State>> acceptedDataTypes = config.getAcceptedDataTypes(); return TypeParser.parseState(acceptedDataTypes, value); } else { return null; } } /** * {@inheritDoc} */ @Override public String getHttpMethod(String itemName, Command command) { HttpBindingConfig config = (HttpBindingConfig) bindingConfigs.get(itemName); return config != null && getConfigElement(config, command) != null ? getConfigElement(config, command).httpMethod : null; } /** * {@inheritDoc} */ @Override public String getUrl(String itemName, Command command) { HttpBindingConfig config = (HttpBindingConfig) bindingConfigs.get(itemName); return config != null && getConfigElement(config, command) != null ? getConfigElement(config, command).url : null; } /** * {@inheritDoc} */ @Override public Properties getHttpHeaders(String itemName, Command command) { HttpBindingConfig config = (HttpBindingConfig) bindingConfigs.get(itemName); return config != null && getConfigElement(config, command) != null ? getConfigElement(config, command).headers : null; } /** * {@inheritDoc} */ @Override public String getUrl(String itemName) { HttpBindingConfig config = (HttpBindingConfig) bindingConfigs.get(itemName); return config != null && config.get(IN_BINDING_KEY) != null ? config.get(IN_BINDING_KEY).url : null; } /** * {@inheritDoc} */ @Override public String getBody(String itemName, Command command) { HttpBindingConfig config = (HttpBindingConfig) bindingConfigs.get(itemName); return config != null && getConfigElement(config, command) != null ? getConfigElement(config, command).body : null; } /** * {@inheritDoc} */ @Override public Properties getHttpHeaders(String itemName) { HttpBindingConfig config = (HttpBindingConfig) bindingConfigs.get(itemName); return config != null && config.get(IN_BINDING_KEY) != null ? config.get(IN_BINDING_KEY).headers : null; } /** * {@inheritDoc} */ @Override public int getRefreshInterval(String itemName) { HttpBindingConfig config = (HttpBindingConfig) 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) { HttpBindingConfig config = (HttpBindingConfig) 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()) { HttpBindingConfig httpConfig = (HttpBindingConfig) bindingConfigs.get(itemName); if (httpConfig.containsKey(IN_BINDING_KEY)) { inBindings.add(itemName); } } return inBindings; } private HttpBindingConfigElement getConfigElement(HttpBindingConfig config, Command command) { if (config.get(command) != null) { return config.get(command); } if (!CHANGED_COMMAND_KEY.equals(command)) { return config.get(WILDCARD_COMMAND_KEY); } return null; } /** * This is an internal data structure to map commands to * {@link HttpBindingConfigElement }. There will be map like * <code>ON->HttpBindingConfigElement</code> */ static class HttpBindingConfig extends HashMap<Command, HttpBindingConfigElement> implements BindingConfig { /** generated serialVersion UID */ private static final long serialVersionUID = 6164971643530954095L; private List<Class<? extends State>> acceptedDataTypes = null; HttpBindingConfig(Item item) { acceptedDataTypes = new ArrayList<Class<? extends State>>(item.getAcceptedDataTypes()); } List<Class<? extends State>> getAcceptedDataTypes() { return acceptedDataTypes; } } /** * This is an internal data structure to store information from the binding * config strings and use it to answer the requests to the HTTP binding * provider. */ static class HttpBindingConfigElement implements BindingConfig { public String httpMethod; public String url; public Properties headers; public int refreshInterval; public String transformation; public String body; @Override public String toString() { return "HttpBindingConfigElement [httpMethod=" + httpMethod + ", url=" + url + ", headers=" + headers + ", refreshInterval=" + refreshInterval + ", transformation=" + transformation + "]"; } } }