View Javadoc
1   /*
2    * Copyright (C) 2021 INRAE AgroClim
3    *
4    * This file is part of Indicators.
5    *
6    * Indicators is free software: you can redistribute it and/or modify
7    * it under the terms of the GNU General Public License as published by
8    * the Free Software Foundation, either version 3 of the License, or
9    * (at your option) any later version.
10   *
11   * Indicators is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14   * GNU General Public License for more details.
15   *
16   * You should have received a copy of the GNU General Public License
17   * along with Indicators. If not, see <http://www.gnu.org/licenses/>.
18   */
19  package fr.inrae.agroclim.indicators.model.criteria;
20  
21  import java.io.IOException;
22  import java.io.ObjectInputStream;
23  import java.util.ArrayList;
24  import java.util.HashMap;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.Set;
28  
29  import fr.inrae.agroclim.indicators.exception.IndicatorsException;
30  import fr.inrae.agroclim.indicators.exception.type.ComputationErrorType;
31  import fr.inrae.agroclim.indicators.exception.type.ResourceErrorType;
32  import fr.inrae.agroclim.indicators.model.ExpressionParameter;
33  import fr.inrae.agroclim.indicators.model.JEXLFormula;
34  import fr.inrae.agroclim.indicators.model.Parameter;
35  import fr.inrae.agroclim.indicators.model.data.DailyData;
36  import fr.inrae.agroclim.indicators.model.data.Resource;
37  import fr.inrae.agroclim.indicators.model.data.Variable;
38  import fr.inrae.agroclim.indicators.util.Doublet;
39  import jakarta.xml.bind.annotation.XmlElement;
40  import jakarta.xml.bind.annotation.XmlRootElement;
41  import jakarta.xml.bind.annotation.XmlType;
42  import lombok.EqualsAndHashCode;
43  import lombok.Getter;
44  import lombok.Setter;
45  import lombok.ToString;
46  import lombok.extern.log4j.Log4j2;
47  
48  /**
49   * Criteria to compare value with threshold or to define variable to use in aggregation indicator (eg.: Sum).
50   *
51   * Last changed : $Date$
52   *
53   * @author $Author$
54   * @version $Revision$
55   */
56  @XmlRootElement
57  @XmlType(propOrder = {"expression", "expressionParameters"})
58  @Log4j2
59  @ToString
60  @EqualsAndHashCode(callSuper = true, of = {"expression", "expressionParameters"})
61  public final class FormulaCriteria extends Criteria {
62  
63      /**
64       * UID for Serializable.
65       */
66      private static final long serialVersionUID = 874604079323338993L;
67  
68      /**
69       * Helper function for JEXL use.
70       *
71       * @param value             value to check
72       * @param minValueInclusive min value, inclusive
73       * @param maxValueInclusive max value, inclusive
74       * @return true if the value is in the range
75       */
76      public static boolean between(final Number value, final Number minValueInclusive, final Number maxValueInclusive) {
77          if (value == null) {
78              return false;
79          }
80          final double v = value.doubleValue();
81          if (minValueInclusive != null) {
82              final double min = minValueInclusive.doubleValue();
83              if (v < min) {
84                  return false;
85              }
86          }
87          if (maxValueInclusive != null) {
88              final double max = maxValueInclusive.doubleValue();
89              if (v > max) {
90                  return false;
91              }
92          }
93          return true;
94      }
95  
96      /**
97       * JEXL expression.
98       */
99      @Getter
100     @Setter
101     private String expression;
102 
103     /**
104      * Parameters for JEXLFormula.
105      */
106     @Getter
107     @Setter
108     @XmlElement(name = "expressionParameter")
109     private List<ExpressionParameter> expressionParameters;
110 
111     /**
112      * org.apache.commons.jexl2.JexlEngine handler.
113      */
114     private transient JEXLFormula formula = new JEXLFormula();
115 
116     /**
117      * Values id of parameter ⮕  value of parameter.
118      */
119     @Setter
120     @Getter
121     private transient Map<String, Double> parametersValues = new HashMap<>();
122 
123     @Override
124     public FormulaCriteria clone() {
125         final FormulaCriteria clone = new FormulaCriteria();
126         clone.expression = expression;
127         if (expressionParameters != null) {
128             clone.expressionParameters = new ArrayList<>();
129             for (final ExpressionParameter p : expressionParameters) {
130                 try {
131                     clone.expressionParameters.add(p.clone());
132                 } catch (final CloneNotSupportedException ex) {
133                     LOGGER.catching(ex);
134                 }
135             }
136         }
137         clone.parametersValues = parametersValues;
138         if (getParameters() != null) {
139             clone.setParameters(getParameters());
140         }
141         return clone;
142     }
143 
144     @Override
145     public boolean eval(final DailyData data) throws IndicatorsException {
146         if (expression == null) {
147             throw new IndicatorsException(ComputationErrorType.FORMULA_EXPRESSION_NULL);
148         }
149         if (data == null) {
150             throw new IndicatorsException(ResourceErrorType.CLIMATE_EMPTY);
151         }
152         formula.setExpression(expression);
153         final Map<String, Double> values = new HashMap<>();
154         getVariables().forEach(variable -> values.put(variable.name(), data.getValue(variable)));
155         if (parametersValues != null) {
156             values.putAll(parametersValues);
157         }
158         if (expressionParameters != null) {
159             expressionParameters.forEach(p -> values.put(p.getName(), p.getValue()));
160         }
161         try {
162             return formula.evaluate(values, Boolean.class);
163         } catch (final IndicatorsException ex) {
164             throw new IndicatorsException(ComputationErrorType.FORMULA, ex, "Failed to evaluate the criteria.");
165         }
166     }
167 
168     @Override
169     public List<Doublet<Parameter, Number>> getParameterDefaults() {
170         if (getParameters() == null) {
171             return List.of();
172         }
173         final List<Doublet<Parameter, Number>> val = new ArrayList<>();
174         if (expressionParameters != null) {
175             expressionParameters.forEach(p -> getParameters().stream() //
176                     .filter(a -> p.getName().equals(a.getAttribute())) //
177                     .findFirst() //
178                     .ifPresent(param -> val.add(Doublet.of(param, p.getValue()))));
179         }
180         return val;
181     }
182 
183     @Override
184     public Set<Variable> getVariables() {
185         formula.setExpression(expression);
186         return formula.getDataVariables();
187     }
188 
189     @Override
190     public boolean isComputable(final Resource<? extends DailyData> res) {
191         if (res == null) {
192             throw new IllegalArgumentException("resource must no be null!");
193         }
194         formula.setExpression(expression);
195         return formula.isValid();
196     }
197 
198     /**
199      * Context: Deserialization does not initialize formula.
200      *
201      * A final field must be initialized either by direct assignment of an initial value or in the constructor. During
202      * deserialization, neither of these are invoked, so initial values for transients must be set in the
203      * 'readObject()' private method that's invoked during deserialization. And for that to work, the transients must
204      * be non-final.
205      *
206      * @param ois input stream from deserialization
207      */
208     private void readObject(final ObjectInputStream ois) throws ClassNotFoundException, IOException {
209         // perform the default de-serialization first
210         ois.defaultReadObject();
211         formula = new JEXLFormula();
212     }
213 
214     @Override
215     public void removeParameter(final Parameter param) {
216         // Do nothing for this type of criteria (override on subclass)
217     }
218 
219     @Override
220     public String toStringTree(final String indent) {
221         final StringBuilder sb = new StringBuilder();
222         sb.append(indent).append("  class: ").append(getClass().getName()).append("\n");
223         sb.append(indent).append("  expression: ").append(expression).append("\n");
224         if (expressionParameters != null) {
225             expressionParameters.forEach(p -> sb.append(indent).append("  expressionParameter: ")
226                     .append(p.getName()).append("=").append(p.getValue()).append("\n"));
227         }
228         return sb.toString();
229     }
230 }