/**
* Copyright (c) 2010-2017 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.modbus.internal;
import static org.apache.commons.lang.StringUtils.isEmpty;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.StandardToStringStyle;
import org.apache.commons.lang.builder.ToStringBuilder;
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.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.TypeParser;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Class describing transformation of a command or state.
*
* Inspired from other openHAB binding "Transformation" classes.
*
* @author Sami Salonen
* @since 1.10.0
*
*/
public class Transformation {
interface TransformationHelperWrapper {
public TransformationService getTransformationService(BundleContext context, String transformationServiceName);
}
private static final class TransformationHelperWrapperImpl implements TransformationHelperWrapper {
@Override
public TransformationService getTransformationService(BundleContext context, String transformationServiceName) {
return TransformationHelper.getTransformationService(context, transformationServiceName);
}
}
public static final String TRANSFORM_DEFAULT = "default";
public static final Transformation IDENTITY_TRANSFORMATION = new Transformation(TRANSFORM_DEFAULT, null, null);
private static final TransformationHelperWrapper DEFAULT_TRANSFORMATION_HELPER = new TransformationHelperWrapperImpl();
/** RegEx to extract and parse a function String <code>'(.*?)\((.*)\)'</code> */
private static final Pattern EXTRACT_FUNCTION_PATTERN = Pattern.compile("(?<service>.*?)\\((?<arg>.*)\\)");
/**
* Ordered list of types that are tried out first when trying to parse transformed command
*/
private final static List<Class<? extends Command>> DEFAULT_TYPES = new ArrayList<>();
static {
DEFAULT_TYPES.add(DecimalType.class);
DEFAULT_TYPES.add(OpenClosedType.class);
DEFAULT_TYPES.add(OnOffType.class);
}
static private final Logger logger = LoggerFactory.getLogger(Transformation.class);
private static StandardToStringStyle toStringStyle = new StandardToStringStyle();
static {
toStringStyle.setUseShortClassName(true);
}
private final String transformation;
private final String transformationServiceName;
private final String transformationServiceParam;
private TransformationHelperWrapper transformationHelper = DEFAULT_TRANSFORMATION_HELPER;
/**
*
* @param transformation either FUN(VAL) (standard transformation syntax), default (identity transformation
* (output equals input)) or some other value (output is a constant). Futhermore, empty string is
* considered the same way as "default".
*/
public Transformation(String transformation) {
this.transformation = transformation;
//
// Parse transformation configuration here on construction, but delay the
// construction of TransformationService to call-time
if (isEmpty(transformation) || transformation.equalsIgnoreCase(TRANSFORM_DEFAULT)) {
// no-op (identity) transformation
transformationServiceName = null;
transformationServiceParam = null;
} else {
Matcher matcher = EXTRACT_FUNCTION_PATTERN.matcher(transformation);
if (matcher.matches()) {
matcher.reset();
matcher.find();
transformationServiceName = matcher.group("service");
transformationServiceParam = matcher.group("arg");
} else {
logger.debug(
"Given transformation configuration '{}' did not match the FUN(VAL) pattern. Transformation output will be constant '{}'",
transformation, transformation);
transformationServiceName = null;
transformationServiceParam = null;
}
}
}
/**
* For testing. Package level visibility on purpose
*
* @param transformation
* @param transformationHelper
*/
Transformation(String transformation, TransformationHelperWrapper transformationHelper) {
this(transformation);
this.transformationHelper = transformationHelper;
}
/**
* For testing, thus package visibility by design
*
* @param transformation
* @param transformationServiceName
* @param transformationServiceParam
*/
Transformation(String transformation, String transformationServiceName, String transformationServiceParam) {
this.transformation = transformation;
this.transformationServiceName = transformationServiceName;
this.transformationServiceParam = transformationServiceParam;
}
private String transform(String value) {
String transformedResponse;
if (hasTransformationService()) {
try {
TransformationService transformationService = this.transformationHelper
.getTransformationService(ModbusActivator.getContext(), transformationServiceName);
if (transformationService != null) {
transformedResponse = transformationService.transform(transformationServiceParam, value);
} else {
transformedResponse = value;
logger.warn("couldn't transform response because transformationService of type '{}' is unavailable",
transformationServiceName);
}
} catch (Exception te) {
logger.error("transformation throws exception [transformation={}, response={}]", transformation, value,
te);
// in case of an error we return the response without any
// transformation
transformedResponse = value;
}
} else if (isIdentityTransform()) {
// identity transformation
transformedResponse = value;
} else {
// pass value as is
transformedResponse = this.transformation;
}
logger.debug("transformed response is '{}'", transformedResponse);
return transformedResponse;
}
private boolean isIdentityTransform() {
return TRANSFORM_DEFAULT.equalsIgnoreCase(this.transformation);
}
/**
* Transform command to another command using this transformation
*
*
*
* @param types types types to used to parse the transformation result. First one to match is used. Some types are
* always tried first even if they are not list in the types (see DEFAULT_TYPES constant)
* @param command
* @return Transformed command, or null if no transformation was possible
*/
public Command transformCommand(List<Class<? extends Command>> types, Command command) {
if (isIdentityTransform()) {
// optimization, do not convert command->string->command if the transformation is identity transform
return command;
}
final String commandAsString = command.toString();
final String transformed = transform(commandAsString);
Command transformedCommand = null;
transformedCommand = TypeParser.parseCommand(DEFAULT_TYPES, transformed);
if (transformedCommand == null) {
transformedCommand = TypeParser.parseCommand(types, transformed);
}
if (transformedCommand == null) {
logger.warn(
"Could not transform item command '{}' to a Command. Command as string '{}', "
+ "transformed string '{}', transformation '{}'",
command, commandAsString, transformed, transformation);
} else {
logger.debug(
"Transformed item command '{}' to a command {}. Command as string '{}', "
+ "transformed string '{}', transformation '{}'",
command, transformedCommand, commandAsString, transformed, transformation);
}
return transformedCommand;
}
/**
* Transform state to another state using this transformation
*
* @param types types to used to parse the transformation result
* @param command
* @return Transformed command, or null if no transformation was possible
*/
public State transformState(List<Class<? extends State>> types, State state) {
if (isIdentityTransform()) {
// optimization, do not convert state->string->state if the transformation is identity transform
return state;
}
final String stateAsString = state.toString();
final String transformed = transform(stateAsString);
State transformedState = TypeParser.parseState(types, transformed);
if (transformedState == null) {
logger.warn(
"Could not transform item state '{}' (of type {}) to a State! Command as string '{}', "
+ "transformed string '{}', transformation '{}'",
state, state.getClass().getSimpleName(), stateAsString, transformed, transformation);
} else {
logger.debug(
"Transformed item state '{}' (of type {}) to a state {} (of type {}). Input state as string '{}', "
+ "transformed string '{}', transformation '{}'",
state, state.getClass().getSimpleName(), transformedState,
transformedState.getClass().getSimpleName(), stateAsString, transformed, transformation);
}
return transformedState;
}
public boolean hasTransformationService() {
return transformationServiceName != null;
}
/**
* For testing only. Package level visibility on purpose
*
* @param transformationHelper
*/
void setTransformationHelper(TransformationHelperWrapper transformationHelper) {
this.transformationHelper = transformationHelper;
}
@Override
public boolean equals(Object obj) {
if (null == obj) {
return false;
}
if (this == obj) {
return true;
}
if (!(obj instanceof Transformation)) {
return false;
}
Transformation that = (Transformation) obj;
EqualsBuilder eb = new EqualsBuilder();
if (hasTransformationService()) {
eb.append(this.transformationServiceName, that.transformationServiceName);
eb.append(this.transformationServiceParam, that.transformationServiceParam);
} else {
eb.append(this.transformation, that.transformation);
}
return eb.isEquals();
}
@Override
public String toString() {
return new ToStringBuilder(this, toStringStyle).append("tranformation", transformation)
.append("transformationServiceName", transformationServiceName)
.append("transformationServiceParam", transformationServiceParam).toString();
}
}