Java tutorial
/** * Copyright (c) 2010-2015, 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.http.internal; import static org.apache.commons.lang.StringUtils.isNotBlank; import static org.openhab.binding.http.internal.HttpGenericBindingProvider.CHANGED_COMMAND_KEY; import java.util.Calendar; import java.util.Dictionary; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang.StringUtils; import org.openhab.binding.http.HttpBindingProvider; 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.DateTimeItem; 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.DateTimeType; 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.Type; import org.openhab.core.types.TypeParser; import org.openhab.io.net.http.HttpUtil; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * An active binding which requests a given URL frequently. * * @author Thomas.Eichstaedt-Engelen * @author Kai Kreuzer * @author Pauli Anttila * @auther Ben Jones * @since 0.6.0 */ public class HttpBinding extends AbstractActiveBinding<HttpBindingProvider> implements ManagedService { static final Logger logger = LoggerFactory.getLogger(HttpBinding.class); protected static final String CONFIG_TIMEOUT = "timeout"; protected static final String CONFIG_GRANULARITY = "granularity"; /** the timeout to use for connecting to a given host (defaults to 5000 milliseconds) */ private int timeout = 5000; /** 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("(.*?)\\((.*)\\)"); /** RegEx to validate a cache config <code>'^(.*?)\\.(url|updateInterval)$'</code> */ private static final Pattern EXTRACT_CACHE_CONFIG_PATTERN = Pattern.compile("^(.*?)\\.(url|updateInterval)$"); /** RegEx to extract and parse a cache config url with headers <code>'(.*?)(\\{.*\\})?'</code> */ private static final Pattern EXTRACT_CACHE_CONFIG_URL = Pattern.compile("(.*?)(\\{.*\\})?"); /** Map table to store cache data */ private Map<String, CacheConfig> itemCache = new HashMap<String, CacheConfig>(); private Object itemCacheLock = new Object(); public HttpBinding() { } /** * @{inheritDoc} */ @Override protected long getRefreshInterval() { return granularity; } @Override protected String getName() { return "HTTP Refresh Service"; } @Override public void activate() { super.activate(); setProperlyConfigured(true); } /** * @{inheritDoc} */ @Override protected void internalReceiveUpdate(String itemName, State newState) { formatAndExecute(itemName, CHANGED_COMMAND_KEY, newState); } /** * @{inheritDoc} */ @Override public void internalReceiveCommand(String itemName, Command command) { formatAndExecute(itemName, command, command); } /** * @{inheritDoc} */ @Override public void execute() { for (HttpBindingProvider provider : providers) { for (String itemName : provider.getInBindingItemNames()) { String url = provider.getUrl(itemName); url = String.format(url, Calendar.getInstance().getTime()); Properties headers = provider.getHttpHeaders(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) { String response = null; // check if special URL is used and data should get from // cache rather than directly from server if (isCacheConfig(url)) { logger.debug("item '{}' is fetched from cache", itemName); response = getCacheData(url); } else if (isValidUrl(url)) { logger.debug("item '{}' is about to be refreshed now", itemName); response = HttpUtil.executeUrl("GET", url, headers, null, null, timeout); } else { logger.debug("item '{}' is not a valid URL or is a cache id yet to be initialised ({})", itemName, url); continue; } if (response == null) { logger.error("No response received from '{}'", url); } else { String transformedResponse; try { String[] parts = splitTransformationConfig(transformation); String transformationType = parts[0]; String transformationFunction = parts[1]; TransformationService transformationService = TransformationHelper .getTransformationService(HttpActivator.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 if (itemType.isAssignableFrom(DateTimeItem.class)) { return DateTimeType.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); } } /** * Finds the corresponding binding provider, replaces formatting markers * in the url (@see java.util.Formatter for further information) and executes * the formatted url. * * @param itemName the item context * @param command the executed command or one of the virtual commands * (see {@link HttpGenericBindingProvider}) * @param value the value to be used by the String.format method */ private void formatAndExecute(String itemName, Command command, Type value) { HttpBindingProvider provider = findFirstMatchingBindingProvider(itemName, command); if (provider == null) { logger.trace("doesn't find matching binding provider [itemName={}, command={}]", itemName, command); return; } String httpMethod = provider.getHttpMethod(itemName, command); String url = provider.getUrl(itemName, command); url = String.format(url, Calendar.getInstance().getTime(), value); if (isNotBlank(httpMethod) && isNotBlank(url)) { HttpUtil.executeUrl(httpMethod, url, provider.getHttpHeaders(itemName, command), null, null, timeout); } } /** * Find the first matching {@link HttpBindingProvider} according to * <code>itemName</code> and <code>command</code>. * * @param itemName * @param command * * @return the matching binding provider or <code>null</code> if no binding * provider could be found */ private HttpBindingProvider findFirstMatchingBindingProvider(String itemName, Command command) { HttpBindingProvider firstMatchingProvider = null; for (HttpBindingProvider provider : this.providers) { String url = provider.getUrl(itemName, command); if (url != null) { firstMatchingProvider = provider; break; } } return firstMatchingProvider; } /** * Check a URL is a valid HTTP request * * @param url * @return true if a valid HTTP request, false otherwise */ private boolean isValidUrl(String url) { if (StringUtils.startsWithIgnoreCase(url, "http://")) return true; if (StringUtils.startsWithIgnoreCase(url, "https://")) return true; return false; } /** * Synchronized access to the item cache. Do a quick check to see if this * <code>cacheId</code> references a cached item. * * @param cacheId * @return true if this <code>cacheId</code> is a cached item, false * otherwise */ private boolean isCacheConfig(String cacheId) { synchronized (itemCacheLock) { return itemCache.containsKey(cacheId); } } /** * Synchronized access to the item cache. Checks the <code>cacheId</code> * is a cached item and returns the cached value. If the cache has * expired, refresh the cache value by making a new HTTP request. * * @param cacheId * @return the cached (or refreshed) dats */ private String getCacheData(String cacheId) { synchronized (itemCacheLock) { // check again in case the cache was cleared in between taking // the lock when checking in isCacheConfig() and now if (!itemCache.containsKey(cacheId)) return null; CacheConfig cacheConfig = itemCache.get(cacheId); long cacheAge = System.currentTimeMillis() - cacheConfig.lastUpdate; boolean cacheNeedsUpdate = cacheAge >= cacheConfig.updateInterval; if (cacheNeedsUpdate) { // update and store data on cache logger.debug("updating cache for '{}' ('{}')", cacheId, cacheConfig.url); cacheConfig.data = HttpUtil.executeUrl("GET", cacheConfig.url, cacheConfig.headers, null, null, timeout); if (cacheConfig.data != null) cacheConfig.lastUpdate = System.currentTimeMillis(); } return cacheConfig.data; } } /** * {@inheritDoc} */ @SuppressWarnings("rawtypes") public void updated(Dictionary config) throws ConfigurationException { synchronized (itemCacheLock) { // clear any existing cache item configs itemCache.clear(); if (config != null) { String timeoutString = (String) config.get(CONFIG_TIMEOUT); if (StringUtils.isNotBlank(timeoutString)) { timeout = Integer.parseInt(timeoutString); } String granularityString = (String) config.get(CONFIG_GRANULARITY); if (StringUtils.isNotBlank(granularityString)) { granularity = Integer.parseInt(granularityString); } // Parse page cache config @SuppressWarnings("unchecked") Enumeration<String> keys = config.keys(); while (keys.hasMoreElements()) { String key = (String) keys.nextElement(); // the config-key enumeration contains additional keys that we // don't want to process here ... if (CONFIG_TIMEOUT.equals(key) || CONFIG_GRANULARITY.equals(key) || "service.pid".equals(key)) { continue; } Matcher matcher = EXTRACT_CACHE_CONFIG_PATTERN.matcher(key); if (!matcher.matches()) { logger.error("given config key '" + key + "' does not follow the expected pattern '<id>.<url|updateInterval>'"); continue; } matcher.reset(); matcher.find(); String cacheId = matcher.group(1); CacheConfig cacheConfig = itemCache.get(cacheId); if (cacheConfig == null) { cacheConfig = new CacheConfig(cacheId); itemCache.put(cacheId, cacheConfig); } String configKey = matcher.group(2); String value = (String) config.get(key); if ("url".equals(configKey)) { matcher = EXTRACT_CACHE_CONFIG_URL.matcher(value); if (!matcher.matches()) { throw new ConfigurationException(configKey, "given config url '" + configKey + "' does not follow the expected pattern '<id>.url[{<headers>}]'"); } cacheConfig.url = matcher.group(1); cacheConfig.headers = parseHttpHeaders(matcher.group(2)); } else if ("updateInterval".equals(configKey)) { cacheConfig.updateInterval = Integer.valueOf(value); } else { throw new ConfigurationException(configKey, "the given configKey '" + configKey + "' is unknown"); } } } } } 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; } /** * Internal data structure for data cache purposes * */ static class CacheConfig { /** Cache item id */ String id; /** URL where data is fetched */ String url; /** HTTP Headers sent with the request */ Properties headers; /** Update interval for cache */ int updateInterval = 0; /** Variable to store cached data */ String data; /** Last time when data is updated */ long lastUpdate; public CacheConfig(String id) { this.id = id; } @Override public String toString() { return "CacheConfig [id=" + id + ", url=" + url + ", update interval=" + updateInterval + "]"; } } }