]> git.basschouten.com Git - openhab-addons.git/blob
e1ef5f06bed3270265548d75f586be0ac2c4e9d9
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.modbus.internal;
14
15 import static org.apache.commons.lang.StringUtils.isEmpty;
16
17 import java.util.ArrayList;
18 import java.util.List;
19 import java.util.Optional;
20 import java.util.regex.Matcher;
21 import java.util.regex.Pattern;
22
23 import org.apache.commons.lang.builder.EqualsBuilder;
24 import org.apache.commons.lang.builder.StandardToStringStyle;
25 import org.apache.commons.lang.builder.ToStringBuilder;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.core.library.types.DecimalType;
29 import org.openhab.core.library.types.OnOffType;
30 import org.openhab.core.library.types.OpenClosedType;
31 import org.openhab.core.transform.TransformationException;
32 import org.openhab.core.transform.TransformationHelper;
33 import org.openhab.core.transform.TransformationService;
34 import org.openhab.core.types.Command;
35 import org.openhab.core.types.State;
36 import org.openhab.core.types.TypeParser;
37 import org.osgi.framework.BundleContext;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 /**
42  * Class describing transformation of a command or state.
43  *
44  * Inspired from other openHAB binding "Transformation" classes.
45  *
46  * @author Sami Salonen - Initial contribution
47  *
48  */
49 @NonNullByDefault
50 public class Transformation {
51
52     public static final String TRANSFORM_DEFAULT = "default";
53     public static final Transformation IDENTITY_TRANSFORMATION = new Transformation(TRANSFORM_DEFAULT, null, null);
54
55     /** RegEx to extract and parse a function String <code>'(.*?)\((.*)\)'</code> */
56     private static final Pattern EXTRACT_FUNCTION_PATTERN = Pattern.compile("(?<service>.*?)\\((?<arg>.*)\\)");
57
58     /**
59      * Ordered list of types that are tried out first when trying to parse transformed command
60      */
61     private static final List<Class<? extends Command>> DEFAULT_TYPES = new ArrayList<>();
62     static {
63         DEFAULT_TYPES.add(DecimalType.class);
64         DEFAULT_TYPES.add(OpenClosedType.class);
65         DEFAULT_TYPES.add(OnOffType.class);
66     }
67
68     private final Logger logger = LoggerFactory.getLogger(Transformation.class);
69
70     private static StandardToStringStyle toStringStyle = new StandardToStringStyle();
71
72     static {
73         toStringStyle.setUseShortClassName(true);
74     }
75
76     private final @Nullable String transformation;
77     private final @Nullable String transformationServiceName;
78     private final @Nullable String transformationServiceParam;
79
80     /**
81      *
82      * @param transformation either FUN(VAL) (standard transformation syntax), default (identity transformation
83      *            (output equals input)) or some other value (output is a constant). Futhermore, empty string is
84      *            considered the same way as "default".
85      */
86     public Transformation(@Nullable String transformation) {
87         this.transformation = transformation;
88         //
89         // Parse transformation configuration here on construction, but delay the
90         // construction of TransformationService to call-time
91         if (isEmpty(transformation) || transformation.equalsIgnoreCase(TRANSFORM_DEFAULT)) {
92             // no-op (identity) transformation
93             transformationServiceName = null;
94             transformationServiceParam = null;
95         } else {
96             Matcher matcher = EXTRACT_FUNCTION_PATTERN.matcher(transformation);
97             if (matcher.matches()) {
98                 matcher.reset();
99                 matcher.find();
100                 transformationServiceName = matcher.group("service");
101                 transformationServiceParam = matcher.group("arg");
102             } else {
103                 logger.debug(
104                         "Given transformation configuration '{}' did not match the FUN(VAL) pattern. Transformation output will be constant '{}'",
105                         transformation, transformation);
106                 transformationServiceName = null;
107                 transformationServiceParam = null;
108             }
109         }
110     }
111
112     /**
113      * For testing, thus package visibility by design
114      *
115      * @param transformation
116      * @param transformationServiceName
117      * @param transformationServiceParam
118      */
119     Transformation(String transformation, @Nullable String transformationServiceName,
120             @Nullable String transformationServiceParam) {
121         this.transformation = transformation;
122         this.transformationServiceName = transformationServiceName;
123         this.transformationServiceParam = transformationServiceParam;
124     }
125
126     public String transform(BundleContext context, String value) {
127         String transformedResponse;
128         String transformationServiceName = this.transformationServiceName;
129         String transformationServiceParam = this.transformationServiceParam;
130
131         if (transformationServiceName != null) {
132             try {
133                 if (transformationServiceParam == null) {
134                     throw new TransformationException(
135                             "transformation service parameter is missing! Invalid transform?");
136                 }
137                 @Nullable
138                 TransformationService transformationService = TransformationHelper.getTransformationService(context,
139                         transformationServiceName);
140                 if (transformationService != null) {
141                     transformedResponse = transformationService.transform(transformationServiceParam, value);
142                 } else {
143                     transformedResponse = value;
144                     logger.warn("couldn't transform response because transformationService of type '{}' is unavailable",
145                             transformationServiceName);
146                 }
147             } catch (TransformationException te) {
148                 logger.error("transformation throws exception [transformation={}, response={}]", transformation, value,
149                         te);
150
151                 // in case of an error we return the response without any
152                 // transformation
153                 transformedResponse = value;
154             }
155         } else if (isIdentityTransform()) {
156             // identity transformation
157             transformedResponse = value;
158         } else {
159             // pass value as is
160             transformedResponse = this.transformation;
161         }
162
163         return transformedResponse == null ? "" : transformedResponse;
164     }
165
166     public boolean isIdentityTransform() {
167         return TRANSFORM_DEFAULT.equalsIgnoreCase(this.transformation);
168     }
169
170     public static Optional<Command> tryConvertToCommand(String transformed) {
171         Optional<Command> transformedCommand = Optional.ofNullable(TypeParser.parseCommand(DEFAULT_TYPES, transformed));
172         return transformedCommand;
173     }
174
175     /**
176      * Transform state to another state using this transformation
177      *
178      * @param context
179      * @param types types to used to parse the transformation result
180      * @param command
181      * @return Transformed command, or null if no transformation was possible
182      */
183     public @Nullable State transformState(BundleContext context, List<Class<? extends State>> types, State state) {
184         // Note that even identity transformations go through the State -> String -> State steps. This does add some
185         // overhead but takes care of DecimalType -> PercentType conversions, for example.
186         final String stateAsString = state.toString();
187         final String transformed = transform(context, stateAsString);
188         return TypeParser.parseState(types, transformed);
189     }
190
191     public boolean hasTransformationService() {
192         return transformationServiceName != null;
193     }
194
195     @Override
196     public boolean equals(@Nullable Object obj) {
197         if (null == obj) {
198             return false;
199         }
200         if (this == obj) {
201             return true;
202         }
203         if (!(obj instanceof Transformation)) {
204             return false;
205         }
206         Transformation that = (Transformation) obj;
207         EqualsBuilder eb = new EqualsBuilder();
208         if (hasTransformationService()) {
209             eb.append(this.transformationServiceName, that.transformationServiceName);
210             eb.append(this.transformationServiceParam, that.transformationServiceParam);
211         } else {
212             eb.append(this.transformation, that.transformation);
213         }
214         return eb.isEquals();
215     }
216
217     @Override
218     public String toString() {
219         return new ToStringBuilder(this, toStringStyle).append("tranformation", transformation)
220                 .append("transformationServiceName", transformationServiceName)
221                 .append("transformationServiceParam", transformationServiceParam).toString();
222     }
223 }