FormulaCriteria.java
/*
* Copyright (C) 2021 INRAE AgroClim
*
* This file is part of Indicators.
*
* Indicators is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Indicators is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Indicators. If not, see <http://www.gnu.org/licenses/>.
*/
package fr.inrae.agroclim.indicators.model.criteria;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import fr.inrae.agroclim.indicators.exception.IndicatorsException;
import fr.inrae.agroclim.indicators.exception.type.ComputationErrorType;
import fr.inrae.agroclim.indicators.exception.type.ResourceErrorType;
import fr.inrae.agroclim.indicators.model.ExpressionParameter;
import fr.inrae.agroclim.indicators.model.JEXLFormula;
import fr.inrae.agroclim.indicators.model.Parameter;
import fr.inrae.agroclim.indicators.model.data.DailyData;
import fr.inrae.agroclim.indicators.model.data.Resource;
import fr.inrae.agroclim.indicators.model.data.Variable;
import fr.inrae.agroclim.indicators.util.Doublet;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.XmlType;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.log4j.Log4j2;
/**
* Criteria to compare value with threshold or to define variable to use in aggregation indicator (eg.: Sum).
*
* Last changed : $Date$
*
* @author $Author$
* @version $Revision$
*/
@XmlRootElement
@XmlType(propOrder = {"expression", "expressionParameters"})
@Log4j2
@ToString
@EqualsAndHashCode(callSuper = true, of = {"expression", "expressionParameters"})
public final class FormulaCriteria extends Criteria {
/**
* UID for Serializable.
*/
private static final long serialVersionUID = 874604079323338993L;
/**
* Helper function for JEXL use.
*
* @param value value to check
* @param minValueInclusive min value, inclusive
* @param maxValueInclusive max value, inclusive
* @return true if the value is in the range
*/
public static boolean between(final Number value, final Number minValueInclusive, final Number maxValueInclusive) {
if (value == null) {
return false;
}
final double v = value.doubleValue();
if (minValueInclusive != null) {
final double min = minValueInclusive.doubleValue();
if (v < min) {
return false;
}
}
if (maxValueInclusive != null) {
final double max = maxValueInclusive.doubleValue();
if (v > max) {
return false;
}
}
return true;
}
/**
* JEXL expression.
*/
@Getter
@Setter
private String expression;
/**
* Parameters for JEXLFormula.
*/
@Getter
@Setter
@XmlElement(name = "expressionParameter")
private List<ExpressionParameter> expressionParameters;
/**
* org.apache.commons.jexl2.JexlEngine handler.
*/
private transient JEXLFormula formula = new JEXLFormula();
/**
* Values id of parameter ⮕ value of parameter.
*/
@Setter
@Getter
private transient Map<String, Double> parametersValues = new HashMap<>();
@Override
public FormulaCriteria clone() {
final FormulaCriteria clone = new FormulaCriteria();
clone.expression = expression;
if (expressionParameters != null) {
clone.expressionParameters = new ArrayList<>();
for (final ExpressionParameter p : expressionParameters) {
try {
clone.expressionParameters.add(p.clone());
} catch (final CloneNotSupportedException ex) {
LOGGER.catching(ex);
}
}
}
clone.parametersValues = parametersValues;
if (getParameters() != null) {
clone.setParameters(getParameters());
}
return clone;
}
@Override
public boolean eval(final DailyData data) throws IndicatorsException {
if (expression == null) {
throw new IndicatorsException(ComputationErrorType.FORMULA_EXPRESSION_NULL);
}
if (data == null) {
throw new IndicatorsException(ResourceErrorType.CLIMATE_EMPTY);
}
formula.setExpression(expression);
final Map<String, Double> values = new HashMap<>();
getVariables().forEach(variable -> values.put(variable.name(), data.getValue(variable)));
if (parametersValues != null) {
values.putAll(parametersValues);
}
if (expressionParameters != null) {
expressionParameters.forEach(p -> values.put(p.getName(), p.getValue()));
}
try {
return formula.evaluate(values, Boolean.class);
} catch (final IndicatorsException ex) {
throw new IndicatorsException(ComputationErrorType.FORMULA, ex, "Failed to evaluate the criteria.");
}
}
@Override
public List<Doublet<Parameter, Number>> getParameterDefaults() {
if (getParameters() == null) {
return List.of();
}
final List<Doublet<Parameter, Number>> val = new ArrayList<>();
if (expressionParameters != null) {
expressionParameters.forEach(p -> getParameters().stream() //
.filter(a -> p.getName().equals(a.getAttribute())) //
.findFirst() //
.ifPresent(param -> val.add(Doublet.of(param, p.getValue()))));
}
return val;
}
@Override
public Set<Variable> getVariables() {
formula.setExpression(expression);
return formula.getDataVariables();
}
@Override
public boolean isComputable(final Resource<? extends DailyData> res) {
if (res == null) {
throw new IllegalArgumentException("resource must no be null!");
}
formula.setExpression(expression);
return formula.isValid();
}
/**
* Context: Deserialization does not initialize formula.
*
* A final field must be initialized either by direct assignment of an initial value or in the constructor. During
* deserialization, neither of these are invoked, so initial values for transients must be set in the
* 'readObject()' private method that's invoked during deserialization. And for that to work, the transients must
* be non-final.
*
* @param ois input stream from deserialization
*/
private void readObject(final ObjectInputStream ois) throws ClassNotFoundException, IOException {
// perform the default de-serialization first
ois.defaultReadObject();
formula = new JEXLFormula();
}
@Override
public void removeParameter(final Parameter param) {
// Do nothing for this type of criteria (override on subclass)
}
@Override
public String toStringTree(final String indent) {
final StringBuilder sb = new StringBuilder();
sb.append(indent).append(" class: ").append(getClass().getName()).append("\n");
sb.append(indent).append(" expression: ").append(expression).append("\n");
if (expressionParameters != null) {
expressionParameters.forEach(p -> sb.append(indent).append(" expressionParameter: ")
.append(p.getName()).append("=").append(p.getValue()).append("\n"));
}
return sb.toString();
}
}