/**
* Copyright (c) 1997, 2016 by ProSyst Software GmbH 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.eclipse.smarthome.automation.core.internal;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.eclipse.smarthome.automation.Action;
import org.eclipse.smarthome.automation.Condition;
import org.eclipse.smarthome.automation.Module;
import org.eclipse.smarthome.config.core.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Resolves Module references.
* They can be
* <ul>
* <li>
* Module configuration property to Rule Configuration property
* </li>
* <li>
* Module configuration property to Composite Module configuration property
* </li>
* <li>
* Module inputs to Composite Module inputs
* </li>
* <li>
* Module inputs to Composite Module Configuration
* </li>
* </ul>
*
* Module 'A' Configuration properties can have references to either CompositeModule Configuration properties or Rule
* Configuration properties depending where Module 'A' is placed.
* <br/>
* Note. If Module 'A' is child of CompositeModule - it cannot have direct configuration references to the Rule that is
* holding the CompositeModule.
* <ul>
* <li>
* Single reference configuration value where whole configuration property value is replaced(if found) with the
* referenced value
* <br/>
* 'configurationProperty': '${singleReference}'
* </li>
* <li>
* Complex reference configuration value where only reference parts are replaced in the whole configuration property
* value.
* <br/>
* 'configurationProperty': '{key1: ${complexReference1}, key2: ${complexReference2}'
* </li>
* </ul>
*
* Given Module 'A' is child of CompositeModule then its inputs can have '${singleReferences}' to CompositeModule.
* <ul>
* <li>
* Single reference to CompositeModule inputs where whole input value is replaced with the referenced value
* <br/>
* 'childInput' : '${compositeModuleInput}'
* </li>
* <li>
* Single reference to CompositeModule configuration where whole input value is replaced with the referenced value
* <br/>
* 'childInput' : '${compositeModuleConfiguration}'
* </li>
* </ul>
*
* @author Vasil Ilchev - Initial Contribution
*/
public class ReferenceResolverUtil {
private static final Logger logger = LoggerFactory.getLogger(ReferenceResolverUtil.class);
private ReferenceResolverUtil() {
}
/**
* Updates (changes) configuration properties of module base on given context (it can be CompositeModule
* Configuration or Rule Configuration).
* For example:
* 1) If a module configuration property has a value '${name}' the method looks for such key in context
* and if found - replace the module's configuration value as it is.
*
* 2) If a module configuration property has complex value 'Hello ${firstName} ${lastName}'
* the method tries to parse it and replace (if values are found) referenced parts in module's configuration value.
* Will try to find values for ${firstName} and ${lastName} in the given context and replace them.
* References that are not found in the context - are not replaced.
*
* @param module module that is directly part of Rule or part of CompositeModule
* @param context containing Rule configuration or Composite configuration values.
*/
public static void updateModuleConfiguration(Module module, Map<String, ?> context) {
Configuration config = module.getConfiguration();
for (String configKey : config.keySet()) {
Object o = config.get(configKey);
if (o instanceof String) {
String childConfigPropertyValue = (String) o;
if (isReference(childConfigPropertyValue)) {
Object result = resolveReference(childConfigPropertyValue, context);
if (result != null) {
config.put(configKey, result);
}
} else if (containsPattern(childConfigPropertyValue)) {
Object result = resolvePattern(childConfigPropertyValue, context);
config.put(configKey, result);
}
}
}
}
/**
* Resolves Composite child module's inputs references to CompositeModule context (inputs and configuration)
*
* @param module Composite Module's child module.
* @param compositeContext Composite Module's context
* @return context for given module ready for execution.
*/
public static Map<String, Object> getCompositeChildContext(Module module, Map<String, ?> compositeContext) {
Map<String, Object> resultContext = new HashMap<String, Object>();
Map<String, String> inputs = null;
if (module instanceof Condition) {
inputs = ((Condition) module).getInputs();
} else if (module instanceof Action) {
inputs = ((Action) module).getInputs();
}
if (inputs != null) {
for (Entry<String, String> input : inputs.entrySet()) {
final String inputName = input.getKey();
final String inputValue = input.getValue();
if (isReference(inputValue)) {
final Object result = resolveReference(inputValue, compositeContext);
resultContext.put(inputName, result);
}
}
}
return resultContext;
}
/**
* Resolves single reference '${singleReference}' from given context.
*
* @param reference single reference to parse
* @param context from where the value will be get
* @return resolved value
*/
public static Object resolveReference(String reference, Map<String, ?> context) {
Object result = reference;
if (isReference(reference)) {
final String trimmedVal = reference.trim();
result = context.get(trimmedVal.substring(2, trimmedVal.length() - 1));// ${substring}
}
return result;
}
/**
* Tries to resolve complex references e.g. 'Hello ${firstName} ${lastName}'..'{key1: ${reference1}, key2:
* ${reference2}}'..etc.
*
* References are keys in the context map (without the '${' prefix and '}' suffix).
*
* If value is found in the given context it overrides the reference part in the configuration value.
* For example:
*
* <pre>
* configuration {
* ..
* configProperty: 'Hello ${firstName} ${lastName}'
* ..
* }
* </pre>
*
* And context that has value for '${lastName}':
*
* <pre>
* ..
* firstName: MyFirstName
* ..
* lastName: MyLastName
* ..
* </pre>
*
* Result will be:
*
* <pre>
* configuration {
* ..
* configProperty: 'Hello MyFirstName MyLastName'
* ..
* }
* </pre>
*
* References for which values are not found in the context - remain as they are in the configuration property.
* (It will not stop resolving the remaining references(if there are) in the configuration property value)
*
* @param reference
* @param context
* @return
*/
private static String resolvePattern(String reference, Map<String, ?> context) {
final StringBuilder sb = new StringBuilder();
int previous = 0;
for (int start, end; (start = reference.indexOf("${", previous)) != -1; previous = end + 1) {
sb.append(reference.substring(previous, start));
end = reference.indexOf('}', start + 2);
if (end == -1) {
previous = start;
logger.warn("Couldn't parse referenced key: " + reference.substring(start)
+ ": expected reference syntax-> ${referencedKey}");
break;
}
final String referencedKey = reference.substring(start + 2, end);
final Object referencedValue = context.get(referencedKey);
if (referencedValue != null) {
sb.append(referencedValue);
} else {
// remain as it is: value is null
sb.append(reference.substring(start, end + 1));
logger.warn("Cannot find reference for ${ {} } , it will remain the same.", referencedKey);
}
}
sb.append(reference.substring(previous));
return sb.toString();
}
/**
* Determines whether given Text is '${reference}'.
*
* @param value to be evaluated
* @return True if this value is a '${reference}', false otherwise.
*/
private static boolean isReference(String value) {
String trimmedVal = value == null ? null : value.trim();
return trimmedVal != null && trimmedVal.lastIndexOf("${") == 0 // starts with '${' and it contains it only once
&& trimmedVal.indexOf('}') == trimmedVal.length() - 1 // contains '}' only once - last char
&& trimmedVal.length() > 3; // reference is not empty '${}'
}
/**
* Determines whether given Text is '.....${reference}...'.
*
* @param value to be evaluated
* @return True if this value is a '.....${reference}...', false otherwise.
*/
private static boolean containsPattern(String value) {
return value != null && value.trim().contains("${") && value.trim().indexOf("${") < value.trim().indexOf("}");
}
/**
* This method tries to extract value from Bean or Map.
* <li>To get Map value, the square brackets have to be used as reference: [x] is equivalent to the call
* ((Map)object).get(x)
* <li>To get Bean value, the dot and property name have to be used as reference: .x is equivalent to the call
* object.getX()
*
* For example: ref = [x].y[z] will execute the call: ((Map)((Map)object).get(x).getY()).get(z)
*
* @param object Bean ot map object
* @param ref reference path to the value
* @return the value when it exist on specified reference path or null otherwise.
*/
public static Object getValue(Object object, String ref) {
Object result = null;
int idx = -1;
if (object == null) {
return null;
}
if ((ref == null) || (ref.length() == 0)) {
return object;
}
char ch = ref.charAt(0);
if ('.' == ch) {
ref = ref.substring(1, ref.length());
}
if ('[' == ch) {
if (!(object instanceof Map)) {
return null;
}
idx = ref.indexOf(']');
if (idx == -1) {
return null;
}
String key = ref.substring(1, idx++);
Map map = (Map) object;
result = map.get(key);
} else {
String key = null;
idx = getNextRefToken(ref, 1);
key = idx != ref.length() ? ref.substring(0, idx) : ref;
String getter = "get" + key.substring(0, 1).toUpperCase() + key.substring(1);
try {
Method m = object.getClass().getMethod(getter, new Class[0]);
if (m != null) {
result = m.invoke(object, null);
} else {
return null;
}
} catch (Exception e) {
return null;
}
}
if ((result != null) && (idx < ref.length())) {
return getValue(result, ref.substring(idx));
}
return result;
}
/**
* Gets the end of current token of reference path.
*
* @param ref reference path used to access value in bean or map objects
* @param startIndex starting point to check for next tokens.
* @return end of current token.
*/
public static int getNextRefToken(String ref, int startIndex) {
int idx1 = ref.indexOf('[', startIndex);
int idx2 = ref.indexOf('.', startIndex);
int idx;
if ((idx1 != -1) && ((idx2 == -1) || (idx1 < idx2))) {
idx = idx1;
} else {
if ((idx2 != -1) && ((idx1 == -1) || (idx2 < idx1))) {
idx = idx2;
} else {
idx = ref.length();
}
}
return idx;
}
}