Package org.openhab.binding.http.internal

Source Code of org.openhab.binding.http.internal.HttpBinding$CacheConfig

/**
* 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.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);
 
  /** 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("timeout");
        if (StringUtils.isNotBlank(timeoutString)) {
          timeout = Integer.parseInt(timeoutString);
        }
       
        String granularityString = (String) config.get("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 ("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 + "]";
    }
  }
}
TOP

Related Classes of org.openhab.binding.http.internal.HttpBinding$CacheConfig

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.