From 66e6a624352b1b84ff04000ca0e4ed983373adab Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Tue, 19 Dec 2023 17:00:36 +0100
Subject: [PATCH 01/20] =?UTF-8?q?D=C3=A9but?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../exception/ErrorMessageException.java      |   4 +-
 ...gory.java => IndicatorsErrorCategory.java} |  14 +-
 .../exception/IndicatorsException.java        |  31 ++++
 .../exception/type/ComputationErrorType.java  |  60 +++++++
 .../exception/type/ResourceErrorType.java     | 132 ++++++++++++++
 .../exception/type/package-info.java          |  20 +++
 .../model/data/ResourceManager.java           | 168 ++----------------
 .../model/indicator/AggregationIndicator.java |   6 +-
 .../indicators/resources/messages.properties  |   2 +
 .../indicators/exception/ErrorTypeTest.java   |  12 +-
 .../model/data/ResourceManagerTest.java       |  40 ++---
 .../model/data/soil/SoilCalculatorTest.java   |  11 +-
 12 files changed, 312 insertions(+), 188 deletions(-)
 rename src/main/java/fr/inrae/agroclim/indicators/exception/{IndicatorErrorCategory.java => IndicatorsErrorCategory.java} (61%)
 create mode 100644 src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorsException.java
 create mode 100644 src/main/java/fr/inrae/agroclim/indicators/exception/type/ComputationErrorType.java
 create mode 100644 src/main/java/fr/inrae/agroclim/indicators/exception/type/ResourceErrorType.java
 create mode 100644 src/main/java/fr/inrae/agroclim/indicators/exception/type/package-info.java

diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/ErrorMessageException.java b/src/main/java/fr/inrae/agroclim/indicators/exception/ErrorMessageException.java
index 0d58a8af..0ac0c704 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/exception/ErrorMessageException.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/ErrorMessageException.java
@@ -8,11 +8,11 @@ import lombok.RequiredArgsConstructor;
  * @author omaury
  */
 @RequiredArgsConstructor
-public class ErrorMessageException extends Exception {
+public abstract class ErrorMessageException extends Exception {
     /**
      * UUID for Serializable.
      */
-    private static final long serialVersionUID = 6030595237342400004L;
+    private static final long serialVersionUID = 6030595237342400005L;
 
     /**
      * The object with details.
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorErrorCategory.java b/src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorsErrorCategory.java
similarity index 61%
rename from src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorErrorCategory.java
rename to src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorsErrorCategory.java
index 70ded7ac..3292bb30 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorErrorCategory.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorsErrorCategory.java
@@ -10,18 +10,22 @@ import lombok.RequiredArgsConstructor;
 /**
  * Category for error types in the Indicators library.
  *
- * Last change $Date$
+ * Last change $Date: 2023-03-16 17:36:45 +0100 (jeu. 16 mars 2023) $
  *
  * @author omaury
- * @author $Author$
- * @version $Revision$
+ * @author $Author: omaury $
+ * @version $Revision: 644 $
  */
 @RequiredArgsConstructor
-public enum IndicatorErrorCategory implements ErrorCategory {
+public enum IndicatorsErrorCategory implements ErrorCategory {
     /**
      * For {@link ResourceManager}.
      */
-    RESOURCES("IND01");
+    RESOURCES("IND01"),
+    /**
+     * While {@link Indicator#compute()}.
+     */
+    COMPUTATION("IND02");
 
     /**
      * Category code: prefix for {@link ErrorType#getFullCode()}.
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorsException.java b/src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorsException.java
new file mode 100644
index 00000000..b910e449
--- /dev/null
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorsException.java
@@ -0,0 +1,31 @@
+package fr.inrae.agroclim.indicators.exception;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * An exception with {@link ErrorMessage} to describe the error in the Indicators library to the user.
+ *
+ * Last changed : $Date: 2023-03-16 17:39:08 +0100 (jeu. 16 mars 2023) $
+ *
+ * @author $Author: omaury $
+ * @version $Revision: 1247 $
+ */
+public class IndicatorsException  extends ErrorMessageException {
+
+    /**
+     * UUID for Serializable.
+     */
+    private static final long serialVersionUID = 6030595237342400006L;
+
+    /**
+     * Constructor.
+     *
+     * @param errorType Error type.
+     * @param arguments Arguments for the message.
+     */
+    public IndicatorsException(final ErrorType errorType, final Serializable... arguments) {
+        super(new ErrorMessage("fr.inrae.agroclim.indicators.resources.messages", errorType, List.of(arguments)));
+    }
+
+}
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/type/ComputationErrorType.java b/src/main/java/fr/inrae/agroclim/indicators/exception/type/ComputationErrorType.java
new file mode 100644
index 00000000..98523c07
--- /dev/null
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/type/ComputationErrorType.java
@@ -0,0 +1,60 @@
+package fr.inrae.agroclim.indicators.exception.type;
+
+import fr.inrae.agroclim.indicators.exception.ErrorCategory;
+import fr.inrae.agroclim.indicators.exception.ErrorType;
+import fr.inrae.agroclim.indicators.exception.IndicatorsErrorCategory;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * Keys from messages.properties used to warn about errors in {@link Indicator#compute()}.
+ */
+@RequiredArgsConstructor
+public enum ComputationErrorType implements ErrorType {
+    /**
+     * Definition of the indicator is not good.
+     */
+    WRONG_DEFINITION(null, "01"),
+    /**
+     * Criteria should be NoCriteria or SimpleCriteria.
+     */
+    CRITERIA_ISNT_NOCRITERIA_SIMPLECRITERIA(WRONG_DEFINITION, "02");
+
+    /**
+     * Parent refers to the resource part.
+     */
+    @Getter
+    private final ComputationErrorType parent;
+
+    /**
+     * Subcode for the error.
+     */
+    @Getter
+    private final String subCode;
+
+    @Override
+    public ErrorCategory getCategory() {
+        return IndicatorsErrorCategory.COMPUTATION;
+    }
+    /**
+     * @return partial I18n key for messages.properties
+     */
+    private String getShortKey() {
+        return name().toLowerCase().replace("_", ".");
+    }
+    /**
+     * @return Key for Resource/I18nResource.
+     */
+    @Override
+    public String getI18nKey() {
+        if (parent != null) {
+            return "error.computation." + parent.getShortKey() + "." + getShortKey();
+        }
+        return "error.computation." + getShortKey();
+    }
+
+    @Override
+    public String getName() {
+        return name();
+    }
+}
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/type/ResourceErrorType.java b/src/main/java/fr/inrae/agroclim/indicators/exception/type/ResourceErrorType.java
new file mode 100644
index 00000000..aef77fda
--- /dev/null
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/type/ResourceErrorType.java
@@ -0,0 +1,132 @@
+package fr.inrae.agroclim.indicators.exception.type;
+
+import fr.inrae.agroclim.indicators.exception.ErrorCategory;
+import fr.inrae.agroclim.indicators.exception.ErrorType;
+import fr.inrae.agroclim.indicators.exception.IndicatorsErrorCategory;
+import lombok.Getter;
+
+/**
+ * Keys from messages.properties used to warn about errors in {@link ResourceManager}.
+ */
+public enum ResourceErrorType implements ErrorType {
+    /**
+     * Climate, topic.
+     */
+    CLIMATE("01", null, "resource.climatic"),
+    /**
+     * No climate data.
+     */
+    CLIMATE_EMPTY("02", CLIMATE, "empty"),
+    /**
+     * Not enough data.
+     */
+    CLIMATE_SIZE_WRONG("03", CLIMATE, "size.wrong"),
+    /**
+     * Years of climate, topic.
+     */
+    CLIMATIC_YEARS("04", null, "resource.climatic.years"),
+    /**
+     * No years of climate.
+     */
+    CLIMATE_YEARS_EMPTY("05", CLIMATIC_YEARS, "empty"),
+    /**
+     * Not enough data.
+     */
+    CLIMATE_YEARS_MISSING("06", CLIMATIC_YEARS, "missing"),
+    /**
+     * Phenology, topic.
+     */
+    PHENO("07", null, "resource.pheno"),
+    /**
+     * No phenological data.
+     */
+    PHENO_EMPTY("08", PHENO, "empty"),
+    /**
+     * Years of phenology, topic.
+     */
+    PHENO_YEARS("09", null, "resource.pheno.years"),
+    /**
+     * No years of phenology.
+     */
+    PHENO_YEARS_EMPTY("10", PHENO_YEARS, "empty"),
+    /**
+     * Not enough data.
+     */
+    PHENO_YEARS_MISSING("11", PHENO_YEARS, "missing"),
+    /**
+     * Resource in general, topic.
+     */
+    RESOURCE("12", null, "resource"),
+    /**
+     * Setting not set.
+     */
+    RESOURCE_CROPDEVELOPMENT_YEARS("13", RESOURCE, "cropdevelopmentyears.null"),
+    /**
+     * Soil, topic.
+     */
+    SOIL("14", null, "resource.soil"),
+    /**
+     * Not enough data.
+     */
+    SOIL_SIZE_WRONG("15", SOIL, "size.wrong"),
+    /**
+     * Variables, topic.
+     */
+    VARIABLES("16", null, "resource.variables"),
+    /**
+     * No variable.
+     */
+    VARIABLES_EMPTY("17", VARIABLES, "empty"),
+    /**
+     * No variale.
+     */
+    VARIABLES_MISSING("18", VARIABLES, "missing");
+    /**
+     * Key for Resource/I18nResource.
+     */
+    private final String key;
+    /**
+     * Subcode for the error.
+     */
+    @Getter
+    private final String subCode;
+    /**
+     * Parent refers to the resource part.
+     */
+    @Getter
+    private final ResourceErrorType parent;
+
+    /**
+     * Constructor.
+     *
+     * @param c Subcode for the error.
+     * @param p Parent refers to the resource part.
+     * @param k Key for Resource/I18nResource.
+     */
+    ResourceErrorType(final String c, final ResourceErrorType p, final String k) {
+        parent = p;
+        key = k;
+        subCode = c;
+    }
+
+    @Override
+    public ErrorCategory getCategory() {
+        return IndicatorsErrorCategory.RESOURCES;
+    }
+
+    /**
+     * @return Key for Resource/I18nResource.
+     */
+    @Override
+    public String getI18nKey() {
+        if (parent != null) {
+            return "error.evaluation." + parent.key + "." + key;
+        }
+        return "error.evaluation." + key;
+    }
+
+    @Override
+    public String getName() {
+        return name();
+    }
+}
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/type/package-info.java b/src/main/java/fr/inrae/agroclim/indicators/exception/type/package-info.java
new file mode 100644
index 00000000..647131bf
--- /dev/null
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/type/package-info.java
@@ -0,0 +1,20 @@
+/**
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+/**
+ * {@link ErrorType} implementations for error handling.
+ */
+package fr.inrae.agroclim.indicators.exception.type;
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/data/ResourceManager.java b/src/main/java/fr/inrae/agroclim/indicators/model/data/ResourceManager.java
index 9d373087..6c091cd1 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/data/ResourceManager.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/data/ResourceManager.java
@@ -26,10 +26,8 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 
-import fr.inrae.agroclim.indicators.exception.ErrorCategory;
 import fr.inrae.agroclim.indicators.exception.ErrorMessage;
-import fr.inrae.agroclim.indicators.exception.ErrorType;
-import fr.inrae.agroclim.indicators.exception.IndicatorErrorCategory;
+import fr.inrae.agroclim.indicators.exception.type.ResourceErrorType;
 import fr.inrae.agroclim.indicators.model.TimeScale;
 import fr.inrae.agroclim.indicators.model.data.Variable.Type;
 import fr.inrae.agroclim.indicators.model.data.climate.ClimaticResource;
@@ -37,7 +35,6 @@ import fr.inrae.agroclim.indicators.model.data.phenology.PhenologicalResource;
 import fr.inrae.agroclim.indicators.util.DateUtils;
 import lombok.Getter;
 import lombok.NonNull;
-import lombok.RequiredArgsConstructor;
 import lombok.Setter;
 import lombok.extern.log4j.Log4j2;
 
@@ -47,141 +44,14 @@ import lombok.extern.log4j.Log4j2;
  *
  * Its responsibility is data storage and checking data consistency.
  *
- * Last changed : $Date$
+ * Last changed : $Date: 2023-03-16 17:36:45 +0100 (jeu. 16 mars 2023) $
  *
- * @author $Author$
- * @version $Revision$
+ * @author $Author: omaury $
+ * @version $Revision: 644 $
  */
 @Log4j2
 public final class ResourceManager implements Serializable, Cloneable {
 
-    /**
-     * Keys from messages.properties used to warn about errors.
-     */
-    @RequiredArgsConstructor
-    public enum ErrorI18nKey implements ErrorType {
-        /**
-         * Climate, topic.
-         */
-        CLIMATE("01", null, "resource.climatic"),
-        /**
-         * No climate data.
-         */
-        CLIMATE_EMPTY("02", CLIMATE, "empty"),
-        /**
-         * Not enough data.
-         */
-        CLIMATE_SIZE_WRONG("03", CLIMATE, "size.wrong"),
-        /**
-         * Years of climate, topic.
-         */
-        CLIMATIC_YEARS("04", null, "resource.climatic.years"),
-        /**
-         * No years of climate.
-         */
-        CLIMATE_YEARS_EMPTY("05", CLIMATIC_YEARS, "empty"),
-        /**
-         * Not enough data.
-         */
-        CLIMATE_YEARS_MISSING("06", CLIMATIC_YEARS, "missing"),
-        /**
-         * Phenology, topic.
-         */
-        PHENO("07", null, "resource.pheno"),
-        /**
-         * No phenological data.
-         */
-        PHENO_EMPTY("08", PHENO, "empty"),
-        /**
-         * Years of phenology, topic.
-         */
-        PHENO_YEARS("09", null, "resource.pheno.years"),
-        /**
-         * No years of phenology.
-         */
-        PHENO_YEARS_EMPTY("10", PHENO_YEARS, "empty"),
-        /**
-         * Not enough data.
-         */
-        PHENO_YEARS_MISSING("11", PHENO_YEARS, "missing"),
-        /**
-         * Resource in general, topic.
-         */
-        RESOURCE("12", null, "resource"),
-        /**
-         * Setting not set.
-         */
-        RESOURCE_CROPDEVELOPMENT_YEARS("13", RESOURCE, "cropdevelopmentyears.null"),
-        /**
-         * Soil, topic.
-         */
-        SOIL("14", null, "resource.soil"),
-        /**
-         * Not enough data.
-         */
-        SOIL_SIZE_WRONG("15", SOIL, "size.wrong"),
-        /**
-         * Variables, topic.
-         */
-        VARIABLES("16", null, "resource.variables"),
-        /**
-         * No variable.
-         */
-        VARIABLES_EMPTY("17", VARIABLES, "empty"),
-        /**
-         * No variale.
-         */
-        VARIABLES_MISSING("18", VARIABLES, "missing");
-        /**
-         * Key for Resource/I18nResource.
-         */
-        private final String key;
-        /**
-         * Subcode for the error.
-         */
-        @Getter
-        private final String subCode;
-        /**
-         * Parent refers to the resource part.
-         */
-        @Getter
-        private final ErrorI18nKey parent;
-
-        /**
-         * Constructor.
-         *
-         * @param c Subcode for the error.
-         * @param p Parent refers to the resource part.
-         * @param k Key for Resource/I18nResource.
-         */
-        ErrorI18nKey(final String c, final ErrorI18nKey p, final String k) {
-            parent = p;
-            key = k;
-            subCode = c;
-        }
-
-        @Override
-        public ErrorCategory getCategory() {
-            return IndicatorErrorCategory.RESOURCES;
-        }
-
-        /**
-         * @return Key for Resource/I18nResource.
-         */
-        @Override
-        public String getI18nKey() {
-            if (parent != null) {
-                return "error.evaluation." + parent.key + "." + key;
-            }
-            return "error.evaluation." + key;
-        }
-
-        @Override
-        public String getName() {
-            return name();
-        }
-    }
-
     /**
      * UUID for Serializable.
      */
@@ -191,8 +61,8 @@ public final class ResourceManager implements Serializable, Cloneable {
      * @param errorI18nKey Message key from .property resource.
      */
     private static void addErrorMessage(
-            final Map<ErrorI18nKey, ErrorMessage> errors,
-            final ErrorI18nKey errorI18nKey) {
+            final Map<ResourceErrorType, ErrorMessage> errors,
+            final ResourceErrorType errorI18nKey) {
         errors.put(errorI18nKey.getParent(), new ErrorMessage(
                 "fr.inrae.agroclim.indicators.resources.messages",
                 errorI18nKey, null));
@@ -259,21 +129,21 @@ public final class ResourceManager implements Serializable, Cloneable {
     /**
      * @return consistency errors
      */
-    public Map<ErrorI18nKey, ErrorMessage> getConsitencyErrors() {
-        final Map<ErrorI18nKey, ErrorMessage> errors = new EnumMap<>(ErrorI18nKey.class);
+    public Map<ResourceErrorType, ErrorMessage> getConsitencyErrors() {
+        final Map<ResourceErrorType, ErrorMessage> errors = new EnumMap<>(ResourceErrorType.class);
         // variables not set ?
         if (variables == null) {
-            addErrorMessage(errors, ErrorI18nKey.VARIABLES_MISSING);
+            addErrorMessage(errors, ResourceErrorType.VARIABLES_MISSING);
             return errors;
         }
         if (variables.isEmpty()) {
-            addErrorMessage(errors, ErrorI18nKey.VARIABLES_EMPTY);
+            addErrorMessage(errors, ResourceErrorType.VARIABLES_EMPTY);
             return errors;
         }
         // empty climate
         final List<Integer> climaticYears = climaticResource.getYears();
         if (hasClimaticVariables() && climaticResource.getData().isEmpty()) {
-            addErrorMessage(errors, ErrorI18nKey.CLIMATE_EMPTY);
+            addErrorMessage(errors, ResourceErrorType.CLIMATE_EMPTY);
         } else {
             // missing days or hours
             final int nbClimatic = climaticResource.getData().size();
@@ -285,12 +155,12 @@ public final class ResourceManager implements Serializable, Cloneable {
                 nb = nb * DateUtils.NB_OF_HOURS_IN_DAY;
             }
             if (nbClimatic != nb) {
-                addErrorMessage(errors, ErrorI18nKey.CLIMATE_SIZE_WRONG);
+                addErrorMessage(errors, ResourceErrorType.CLIMATE_SIZE_WRONG);
             }
         }
         // empty phenology
         if (phenologicalResource.isEmpty()) {
-            addErrorMessage(errors, ErrorI18nKey.PHENO_EMPTY);
+            addErrorMessage(errors, ResourceErrorType.PHENO_EMPTY);
         }
         if (!errors.isEmpty()) {
             return errors;
@@ -298,16 +168,16 @@ public final class ResourceManager implements Serializable, Cloneable {
         // period
         final List<Integer> phenoYears = phenologicalResource.getYears();
         if (hasClimaticVariables() && climaticYears.isEmpty()) {
-            addErrorMessage(errors, ErrorI18nKey.CLIMATE_YEARS_EMPTY);
+            addErrorMessage(errors, ResourceErrorType.CLIMATE_YEARS_EMPTY);
         }
         if (phenoYears.isEmpty()) {
-            addErrorMessage(errors, ErrorI18nKey.PHENO_YEARS_EMPTY);
+            addErrorMessage(errors, ResourceErrorType.PHENO_YEARS_EMPTY);
         }
         if (!errors.isEmpty()) {
             return errors;
         }
         if (cropDevelopmentYears == null) {
-            addErrorMessage(errors, ErrorI18nKey.RESOURCE_CROPDEVELOPMENT_YEARS);
+            addErrorMessage(errors, ResourceErrorType.RESOURCE_CROPDEVELOPMENT_YEARS);
             return errors;
         }
         // Phenology data drives evaluation, so
@@ -325,9 +195,9 @@ public final class ResourceManager implements Serializable, Cloneable {
             theMissing.removeAll(climaticYears);
             final ErrorMessage error = new ErrorMessage(
                     "fr.inrae.agroclim.indicators.resources.messages",
-                    ErrorI18nKey.CLIMATE_YEARS_MISSING,
+                    ResourceErrorType.CLIMATE_YEARS_MISSING,
                     theMissing);
-            errors.put(ErrorI18nKey.CLIMATE_YEARS_MISSING.getParent(), error);
+            errors.put(ResourceErrorType.CLIMATE_YEARS_MISSING.getParent(), error);
         }
         if (!errors.isEmpty()) {
             return errors;
@@ -340,7 +210,7 @@ public final class ResourceManager implements Serializable, Cloneable {
             }
             final int nbSoil = climaticResource.getData().size();
             if (nbSoil != nbDays) {
-                addErrorMessage(errors, ErrorI18nKey.SOIL_SIZE_WRONG);
+                addErrorMessage(errors, ResourceErrorType.SOIL_SIZE_WRONG);
             }
         }
         if (errors.isEmpty()) {
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/AggregationIndicator.java b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/AggregationIndicator.java
index 60969fdb..e48ad253 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/AggregationIndicator.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/AggregationIndicator.java
@@ -8,7 +8,9 @@ import java.util.stream.DoubleStream;
 import javax.xml.bind.annotation.XmlElement;
 
 import fr.inrae.agroclim.indicators.exception.FunctionalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.type.ComputationErrorType;
 import fr.inrae.agroclim.indicators.model.criteria.Criteria;
 import fr.inrae.agroclim.indicators.model.criteria.NoCriteria;
 import fr.inrae.agroclim.indicators.model.criteria.SimpleCriteria;
@@ -24,7 +26,7 @@ import lombok.Setter;
  * Aggregate value defined in a {@link SimpleCriteria} using a {@link Function}
  * on {@link DoubleStream} created for {@link AggregationIndicator#variable}.
  *
- * Last changed : $Date$
+ * Last changed : $Date: 2023-06-16 10:36:46 +0200 (ven., 16 juin 2023) $
  *
  * @author omaury
  */
@@ -84,7 +86,7 @@ public abstract class AggregationIndicator extends SimpleIndicatorWithCriteria i
                     .filter(predicate) //
                     .mapToDouble(criteria::getValueOf);
         } else {
-            throw new FunctionalException("criteria is neither NoCriteria nor SimpleCriteria!");
+            throw new IndicatorsException(ComputationErrorType.CRITERIA_ISNT_NOCRITERIA_SIMPLECRITERIA, getId());
         }
         return aggregate(stream);
     }
diff --git a/src/main/resources/fr/inrae/agroclim/indicators/resources/messages.properties b/src/main/resources/fr/inrae/agroclim/indicators/resources/messages.properties
index b9d6735b..aed9f283 100644
--- a/src/main/resources/fr/inrae/agroclim/indicators/resources/messages.properties
+++ b/src/main/resources/fr/inrae/agroclim/indicators/resources/messages.properties
@@ -20,6 +20,8 @@ error.climate.dates=For the year {0}, available dates are from {1} to {2}.
 error.climate.missing=No climatic daily data for phase {0}-{1} (days {2} -> {3}) in {4}.
 error.climate.no.data=No data were retrieved.
 error.climate.wrong.headers=Wrong number of headers! {0} headers are in the file: {1}, but {2} headers are defined: {3}.
+error.computation.wrong.definition=The indicator "{0}" is not well defined: {1}.
+error.computation.wrong.definition.criteria.isnt.nocriteria.simplecriteria=criteria is neither NoCriteria nor SimpleCriteria!
 error.evaluation.resource=Error for resources!
 error.evaluation.resource.climatic=Error for climatic data set in resources!
 error.evaluation.resource.climatic.empty=No climatic data set in resources!
diff --git a/src/test/java/fr/inrae/agroclim/indicators/exception/ErrorTypeTest.java b/src/test/java/fr/inrae/agroclim/indicators/exception/ErrorTypeTest.java
index 695c625b..00cda328 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/exception/ErrorTypeTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/exception/ErrorTypeTest.java
@@ -1,5 +1,7 @@
 package fr.inrae.agroclim.indicators.exception;
 
+import fr.inrae.agroclim.indicators.exception.type.ComputationErrorType;
+import fr.inrae.agroclim.indicators.exception.type.ResourceErrorType;
 import static org.junit.Assert.assertTrue;
 
 import java.util.ArrayList;
@@ -15,16 +17,15 @@ import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
-import fr.inrae.agroclim.indicators.model.data.ResourceManager;
 import fr.inrae.agroclim.indicators.resources.I18n;
 
 /**
  * Ensure all implementations of {@link ErrorType} are well defined.
  *
- * Last changed : $Date$
+ * Last changed : $Date: 2023-03-16 17:36:45 +0100 (jeu. 16 mars 2023) $
  *
- * @author $Author$
- * @version $Revision$
+ * @author $Author: omaury $
+ * @version $Revision: 644 $
  */
 @RunWith(Parameterized.class)
 public class ErrorTypeTest {
@@ -38,7 +39,7 @@ public class ErrorTypeTest {
      */
     @Parameterized.Parameters
     public static List<Class<? extends Enum<?>>> data() {
-        return Arrays.asList(ResourceManager.ErrorI18nKey.class);
+        return List.of(ComputationErrorType.class, ResourceErrorType.class);
     }
 
     /**
@@ -84,6 +85,7 @@ public class ErrorTypeTest {
             if (codes.contains(k.getSubCode())) {
                 duplicates.add(k.getSubCode());
             }
+            codes.add(k.getSubCode());
         }
         assertTrue(duplicates.isEmpty());
     }
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/data/ResourceManagerTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/data/ResourceManagerTest.java
index 6880d2ba..d69f5cfe 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/data/ResourceManagerTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/data/ResourceManagerTest.java
@@ -31,17 +31,17 @@ import java.util.Set;
 import org.junit.Test;
 
 import fr.inrae.agroclim.indicators.exception.ErrorMessage;
-import fr.inrae.agroclim.indicators.model.data.ResourceManager.ErrorI18nKey;
+import fr.inrae.agroclim.indicators.exception.type.ResourceErrorType;
 import fr.inrae.agroclim.indicators.model.data.phenology.AnnualStageData;
 import fr.inrae.agroclim.indicators.resources.Messages;
 
 /**
  * Test the resource manager.
  *
- * Last changed : $Date$
+ * Last changed : $Date: 2023-03-16 17:36:45 +0100 (jeu. 16 mars 2023) $
  *
- * @author $Author$
- * @version $Revision$
+ * @author $Author: omaury $
+ * @version $Revision: 644 $
  */
 public final class ResourceManagerTest extends DataTestHelper {
 
@@ -53,11 +53,11 @@ public final class ResourceManagerTest extends DataTestHelper {
         final ResourceManager mgr = new ResourceManager();
 
         // without variables
-        Map<ErrorI18nKey, ErrorMessage> errors = mgr.getConsitencyErrors();
+        Map<ResourceErrorType, ErrorMessage> errors = mgr.getConsitencyErrors();
         assertNotNull("Empty ResourceManager must return consistency errors!",
                 errors);
-        final Set<ErrorI18nKey> expectedErrors = new HashSet<>();
-        expectedErrors.add(ErrorI18nKey.VARIABLES);
+        final Set<ResourceErrorType> expectedErrors = new HashSet<>();
+        expectedErrors.add(ResourceErrorType.VARIABLES);
         assertEquals("ResourceManager without variables must have errors!",
                 expectedErrors, errors.keySet());
         expectedErrors.clear();
@@ -67,8 +67,8 @@ public final class ResourceManagerTest extends DataTestHelper {
         variables.add(Variable.TMEAN);
         variables.add(Variable.SOILWATERCONTENT);
         mgr.setVariables(variables);
-        expectedErrors.addAll(Arrays.asList(ErrorI18nKey.CLIMATE,
-                ErrorI18nKey.PHENO));
+        expectedErrors.addAll(Arrays.asList(ResourceErrorType.CLIMATE,
+                ResourceErrorType.PHENO));
         errors = mgr.getConsitencyErrors();
         assertNotNull("Empty ResourceManager must return consistency errors!",
                 errors);
@@ -89,14 +89,14 @@ public final class ResourceManagerTest extends DataTestHelper {
                 "radiation", "rain", "tmean", "tmin", "tmax", "rh", "wind"};
         mgr.getClimaticResource().setData(getClimaticData(
                 "climate-missing-data.csv", ";", headers));
-        final Map<ErrorI18nKey, ErrorMessage> errors = mgr.getConsitencyErrors();
+        final Map<ResourceErrorType, ErrorMessage> errors = mgr.getConsitencyErrors();
         final String subject = "ResourceManager with missing days in climatic "
                 + "resource ";
         assertNotNull(subject + "must return consistency errors!", errors);
         assertTrue(subject + "must have errors on climatic resources!",
-                errors.keySet().contains(ErrorI18nKey.CLIMATE));
+                errors.keySet().contains(ResourceErrorType.CLIMATE));
         assertEquals(subject + "must have errors on climatic resources!",
-                errors.get(ErrorI18nKey.CLIMATE).getType().getI18nKey(), ErrorI18nKey.CLIMATE_SIZE_WRONG.getI18nKey());
+                errors.get(ResourceErrorType.CLIMATE).getType().getI18nKey(), ResourceErrorType.CLIMATE_SIZE_WRONG.getI18nKey());
     }
 
     /**
@@ -111,7 +111,7 @@ public final class ResourceManagerTest extends DataTestHelper {
         mgr.setVariables(variables);
         mgr.getClimaticResource().setData(getClimatic2015Data());
         mgr.getPhenologicalResource().setData(getPheno2015Data());
-        final Map<ErrorI18nKey, ErrorMessage> errors = mgr.getConsitencyErrors();
+        final Map<ResourceErrorType, ErrorMessage> errors = mgr.getConsitencyErrors();
         assertNull(errors);
     }
 
@@ -125,11 +125,11 @@ public final class ResourceManagerTest extends DataTestHelper {
         variables.add(Variable.TMEAN);
         mgr.setVariables(variables);
         mgr.getClimaticResource().setData(getClimatic2015Data());
-        final Map<ErrorI18nKey, ErrorMessage> errors = mgr.getConsitencyErrors();
+        final Map<ResourceErrorType, ErrorMessage> errors = mgr.getConsitencyErrors();
         final String subject = "ResourceManager with only climatic resource ";
         assertNotNull(subject + "must return consistency errors!", errors);
-        final Set<ErrorI18nKey> expectedErrors = new HashSet<>();
-        expectedErrors.add(ErrorI18nKey.PHENO);
+        final Set<ResourceErrorType> expectedErrors = new HashSet<>();
+        expectedErrors.add(ResourceErrorType.PHENO);
         assertEquals(subject + "must have errors on pheno resources",
                 expectedErrors, errors.keySet());
     }
@@ -148,10 +148,10 @@ public final class ResourceManagerTest extends DataTestHelper {
         final List<AnnualStageData> data = getPhenoSampleData();
         data.remove(data.size() - 1);
         mgr.getPhenologicalResource().setData(data);
-        final Map<ErrorI18nKey, ErrorMessage> errors = mgr.getConsitencyErrors();
+        final Map<ResourceErrorType, ErrorMessage> errors = mgr.getConsitencyErrors();
         assertNotNull(errors);
-        final Set<ErrorI18nKey> expectedErrors = new HashSet<>();
-        expectedErrors.add(ErrorI18nKey.CLIMATIC_YEARS);
+        final Set<ResourceErrorType> expectedErrors = new HashSet<>();
+        expectedErrors.add(ResourceErrorType.CLIMATIC_YEARS);
         final String subject = "ResourceManager with only climatic data in 2015 "
                 + "must have errors on climatic resources";
         assertEquals(subject, expectedErrors, errors.keySet());
@@ -162,7 +162,7 @@ public final class ResourceManagerTest extends DataTestHelper {
      */
     @Test
     public void error18nKey() {
-        for (final ErrorI18nKey val : ErrorI18nKey.values()) {
+        for (final ResourceErrorType val : ResourceErrorType.values()) {
             if (val.getParent() != null) {
                 assertFalse(val.getI18nKey() + " must be translated!", Messages.get(val.getI18nKey()).startsWith("!"));
             }
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/data/soil/SoilCalculatorTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/data/soil/SoilCalculatorTest.java
index dccd1bab..5f87c24c 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/data/soil/SoilCalculatorTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/data/soil/SoilCalculatorTest.java
@@ -18,6 +18,7 @@ package fr.inrae.agroclim.indicators.model.data.soil;
 
 import fr.inrae.agroclim.indicators.exception.ErrorMessage;
 import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.type.ResourceErrorType;
 import fr.inrae.agroclim.indicators.model.Evaluation;
 import fr.inrae.agroclim.indicators.model.EvaluationSettings;
 import static org.junit.Assert.assertEquals;
@@ -51,10 +52,10 @@ import java.util.TimeZone;
 /**
  * Test Soil data calculator from ClimaticDailyData.
  *
- * Last changed : $Date$
+ * Last changed : $Date: 2023-05-30 15:49:13 +0200 (mar. 30 mai 2023) $
  *
- * @author $Author$
- * @version $Revision$
+ * @author $Author: omaury $
+ * @version $Revision: 656 $
  */
 public final class SoilCalculatorTest extends DataTestHelper {
 
@@ -273,7 +274,7 @@ public final class SoilCalculatorTest extends DataTestHelper {
         final int haltComparison = 582;
         final double tolerance = 0.01;
         final String diffMsg = """
-                               
+
                                At %d-%02d-%02d (%d), %s=%.3f != expected %.3f!""";
         List<String> computationErrors = new ArrayList<>();
         int i = 0;
@@ -371,7 +372,7 @@ public final class SoilCalculatorTest extends DataTestHelper {
         settings.getSoilLoader().setCalculator(calc);
         // 4. initialize resources
         evaluation.initializeResources();
-        final Map<ResourceManager.ErrorI18nKey, ErrorMessage> errors;
+        final Map<ResourceErrorType, ErrorMessage> errors;
         errors = evaluation.getResourceManager().getConsitencyErrors();
         assertNull("No error must be found", errors);
         // Tests
-- 
GitLab


From d5efe59d12d63bbd57d29ea72cca51f38f81b545 Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Wed, 20 Dec 2023 16:18:16 +0100
Subject: [PATCH 02/20] Continuer

---
 .gitlab-ci.yml                                |  2 +-
 .../exception/ErrorMessageException.java      | 17 ++++++
 .../exception/FunctionalException.java        |  3 +
 .../exception/IndicatorsException.java        | 13 ++++
 .../exception/TechnicalException.java         |  3 +
 .../exception/ThrowingToDoubleFunction.java   | 40 ++++++++++++
 .../exception/type/ComputationErrorType.java  | 61 ++++++++++++++++++-
 .../exception/type/ResourceErrorType.java     | 37 +++++------
 .../agroclim/indicators/model/Evaluation.java | 55 +++++++----------
 .../indicators/model/JEXLFormula.java         | 37 +++++------
 .../indicators/model/Quantifiable.java        | 11 +---
 .../model/criteria/ComparisonCriteria.java    |  8 ++-
 .../model/criteria/CompositeCriteria.java     | 13 ++--
 .../indicators/model/criteria/Criteria.java   |  7 +--
 .../model/criteria/FormulaCriteria.java       | 20 +++---
 .../indicators/model/criteria/NoCriteria.java |  4 +-
 .../model/criteria/SimpleCriteria.java        |  9 ++-
 .../model/criteria/VariableCriteria.java      | 21 ++++---
 .../aggregation/AggregationFunction.java      |  7 +--
 .../function/aggregation/JEXLFunction.java    |  4 +-
 .../model/indicator/AggregationIndicator.java | 52 +++++++++-------
 .../model/indicator/AverageOfDiff.java        |  6 +-
 .../model/indicator/CompositeIndicator.java   | 11 ++--
 .../indicators/model/indicator/DayOfYear.java |  9 ++-
 .../indicators/model/indicator/DiffOfSum.java |  6 +-
 .../indicators/model/indicator/Formula.java   | 16 +++--
 .../indicators/model/indicator/Frequency.java |  6 +-
 .../model/indicator/InjectedParameter.java    |  6 +-
 .../model/indicator/MaxWaveLength.java        | 11 ++--
 .../model/indicator/NumberOfDays.java         |  6 +-
 .../model/indicator/NumberOfWaves.java        | 11 ++--
 .../model/indicator/PhaseLength.java          |  6 +-
 .../PotentialSowingDaysFrequency.java         | 12 ++--
 .../indicators/model/indicator/Quotient.java  | 28 +++------
 .../model/indicator/SimpleIndicator.java      | 23 ++-----
 .../indicators/model/indicator/Tamm.java      |  6 +-
 .../model/CulturalPracticesTest.java          |  5 +-
 .../model/EvaluationHourlyTest.java           |  5 +-
 .../model/EvaluationRobertTest.java           |  5 +-
 .../indicators/model/EvaluationTest.java      | 21 +++----
 .../EvaluationWithoutAggregationTest.java     |  9 +--
 .../model/EvalutationCustomHeadersTest.java   |  5 +-
 .../indicators/model/JEXLFormulaTest.java     |  6 +-
 .../indicators/model/KnowledgeTest.java       |  1 +
 .../indicators/model/MMarjouTest.java         |  5 +-
 .../indicators/model/RaidayMeantTest.java     |  8 +--
 .../model/StageDeltaEvaluationTest.java       |  4 +-
 .../model/criteria/CompositeCriteriaTest.java | 22 +++----
 .../model/criteria/FormulaCriteriaTest.java   |  9 +--
 .../model/criteria/SimpleCriteriaTest.java    | 11 ++--
 .../aggregation/JEXLFunctionTest.java         | 20 +++---
 .../model/indicator/AverageOfDiffTest.java    |  5 +-
 .../model/indicator/AverageTest.java          |  6 +-
 .../model/indicator/ColdsumtminTest.java      |  5 +-
 .../model/indicator/DayOfYearTest.java        |  7 +--
 .../model/indicator/DiffOfSumTest.java        |  6 +-
 .../model/indicator/FormulaTest.java          |  7 +--
 .../model/indicator/FrequencyTest.java        |  7 +--
 .../model/indicator/IndicatorTest.java        |  7 +--
 .../model/indicator/MaxWaveLengthTest.java    | 21 +++----
 .../model/indicator/NumberOfDaysTest.java     | 11 ++--
 .../model/indicator/NumberOfWavesTest.java    | 19 ++----
 .../model/indicator/PhaseLengthTest.java      |  5 +-
 .../indicator/PhotothermalQuotientTest.java   |  7 +--
 .../model/indicator/QuotientTest.java         |  7 +--
 .../indicators/model/indicator/SumTest.java   |  7 +--
 66 files changed, 454 insertions(+), 396 deletions(-)
 create mode 100644 src/main/java/fr/inrae/agroclim/indicators/exception/ThrowingToDoubleFunction.java

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 119da58a..d4d6140b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -20,7 +20,7 @@ build_job:
   stage: build
   script: 
     - echo "Maven compile started"
-    - mvn compile
+    - mvn compile test-compile
     - ls -lha /usr/bin/tokei
     - /usr/bin/tokei --version
 
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/ErrorMessageException.java b/src/main/java/fr/inrae/agroclim/indicators/exception/ErrorMessageException.java
index 0ac0c704..524fb1ac 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/exception/ErrorMessageException.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/ErrorMessageException.java
@@ -1,5 +1,6 @@
 package fr.inrae.agroclim.indicators.exception;
 
+import lombok.Getter;
 import lombok.RequiredArgsConstructor;
 
 /**
@@ -17,8 +18,24 @@ public abstract class ErrorMessageException extends Exception {
     /**
      * The object with details.
      */
+    @Getter
     private final ErrorMessage errorMessage;
 
+    /**
+     * Constructor.
+     *
+     * @param message The object with details.
+     * @param  cause the cause (which is saved for later retrieval by the
+     *         {@link #getCause()} method).  (A {@code null} value is
+     *         permitted, and indicates that the cause is nonexistent or
+     *         unknown.)
+
+     */
+    public ErrorMessageException(final ErrorMessage message, final Throwable cause) {
+        super("", cause);
+        this.errorMessage = message;
+    }
+
     @Override
     public final String getMessage() {
         return errorMessage.getMessage();
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/FunctionalException.java b/src/main/java/fr/inrae/agroclim/indicators/exception/FunctionalException.java
index e36e2d1d..831c2c77 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/exception/FunctionalException.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/FunctionalException.java
@@ -18,7 +18,10 @@ package fr.inrae.agroclim.indicators.exception;
 
 /**
  * For Indicator, Evaluation and AggregationFunction.
+ *
+ * @deprecated Use {@link IndicatorsException}.
  */
+@Deprecated
 public class FunctionalException extends AbstractException {
     /**
      * UUID for Serializable.
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorsException.java b/src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorsException.java
index b910e449..5d143c17 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorsException.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorsException.java
@@ -28,4 +28,17 @@ public class IndicatorsException  extends ErrorMessageException {
         super(new ErrorMessage("fr.inrae.agroclim.indicators.resources.messages", errorType, List.of(arguments)));
     }
 
+    /**
+     * Constructor.
+     *
+     * @param errorType Error type.
+     * @param arguments Arguments for the message.
+     * @param  cause the cause (which is saved for later retrieval by the
+     *         {@link #getCause()} method).  (A {@code null} value is
+     *         permitted, and indicates that the cause is nonexistent or
+     *         unknown.)
+     */
+    public IndicatorsException(final ErrorType errorType, final Throwable cause, final Serializable... arguments) {
+        super(new ErrorMessage("fr.inrae.agroclim.indicators.resources.messages", errorType, List.of(arguments)), cause);
+    }
 }
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/TechnicalException.java b/src/main/java/fr/inrae/agroclim/indicators/exception/TechnicalException.java
index 6915c8af..ecee955e 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/exception/TechnicalException.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/TechnicalException.java
@@ -18,7 +18,10 @@ package fr.inrae.agroclim.indicators.exception;
 
 /**
  * For XMLUtils.
+ *
+ * @deprecated Use {@link IndicatorsException}.
  */
+@Deprecated
 public class TechnicalException extends AbstractException {
 
     /**
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/ThrowingToDoubleFunction.java b/src/main/java/fr/inrae/agroclim/indicators/exception/ThrowingToDoubleFunction.java
new file mode 100644
index 00000000..b96cf2c4
--- /dev/null
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/ThrowingToDoubleFunction.java
@@ -0,0 +1,40 @@
+package fr.inrae.agroclim.indicators.exception;
+
+import java.util.function.ToDoubleFunction;
+
+/**
+ * ToDoubleFunction to handle exceptions.
+ *
+ * Inspired by https://www.baeldung.com/java-lambda-exceptions.
+ *
+ * @author Olivier Maury
+ * @param <T> the type of the input to the function
+ * @param <E> the type of the thrown exception
+ */
+@FunctionalInterface
+public interface ThrowingToDoubleFunction<T, E extends Exception> {
+
+    /**
+     * Applies this function to the given argument.
+     *
+     * @param value the function argument
+     * @return the function result
+     * @throws E exception
+     */
+    double applyAsDouble(T value) throws E;
+
+    /**
+     * @param <U> the type of the input to the function
+     * @param throwingFunction the function
+     * @return function which throws checked exception
+     */
+    static <U> ToDoubleFunction wrap(final ThrowingToDoubleFunction<U, Exception> throwingFunction) {
+        return (ToDoubleFunction) (Object value) -> {
+            try {
+                return throwingFunction.applyAsDouble((U) value);
+            } catch (final Exception ex) {
+                throw new RuntimeException(ex);
+            }
+        };
+    }
+}
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/type/ComputationErrorType.java b/src/main/java/fr/inrae/agroclim/indicators/exception/type/ComputationErrorType.java
index 98523c07..c940efa6 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/exception/type/ComputationErrorType.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/type/ComputationErrorType.java
@@ -3,6 +3,7 @@ package fr.inrae.agroclim.indicators.exception.type;
 import fr.inrae.agroclim.indicators.exception.ErrorCategory;
 import fr.inrae.agroclim.indicators.exception.ErrorType;
 import fr.inrae.agroclim.indicators.exception.IndicatorsErrorCategory;
+import fr.inrae.agroclim.indicators.model.indicator.CompositeIndicator;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
 
@@ -14,11 +15,67 @@ public enum ComputationErrorType implements ErrorType {
     /**
      * Definition of the indicator is not good.
      */
-    WRONG_DEFINITION(null, "01"),
+    WRONG_DEFINITION(null, "001"),
+    /**
+     * When an indicator fails to compute.
+     */
+    COMPUTATION(null, "100"),
+    /**
+     * When an indicator in a {@link CompositeIndicator} fails to compute.
+     */
+    COMPOSITE_COMPUTATION(COMPUTATION, "101"),
     /**
      * Criteria should be NoCriteria or SimpleCriteria.
      */
-    CRITERIA_ISNT_NOCRITERIA_SIMPLECRITERIA(WRONG_DEFINITION, "02");
+    CRITERIA_ISNT_NOCRITERIA_SIMPLECRITERIA(WRONG_DEFINITION, "002"),
+    /**
+     * Criteria should not be null.
+     */
+    CRITERIA_NULL(WRONG_DEFINITION, "003"),
+    /**
+     * Daily data must not be null.
+     */
+    DATA_NULL(COMPUTATION, "113"),
+    /**
+     * Dividend indicator should never be null.
+     */
+    QUOTIENT_DIVIDEND_NULL(WRONG_DEFINITION, "005"),
+    /**
+     * Computation of dividend failed.
+     */
+    QUOTIENT_DIVIDEND_EXCEPTION(COMPUTATION, "110"),
+    /**
+     * Computation of divisor failed.
+     */
+    QUOTIENT_DIVISOR_EXCEPTION(COMPUTATION, "111"),
+    /**
+     * Cannot compute quotient as result of divisor is zero.
+     */
+    QUOTIENT_DIVISOR_ZERO(COMPUTATION, "112"),
+    /**
+     * Divisor indicator should never be null.
+     */
+    QUOTIENT_DIVISOR_NULL(WRONG_DEFINITION, "006"),
+    FORMULA(null, "200"),
+    FORMULA_AGGREGATION_NULL(FORMULA, "211"),
+    FORMULA_EXPRESSION_NULL(FORMULA, "221"),
+    FORMULA_EXPRESSION_BLANK(FORMULA, "222"),
+    FORMULA_EXPRESSION_PARENTHESIS(FORMULA, "223"),
+    FORMULA_EXPRESSION_PARSING(FORMULA, "224"),
+    FORMULA_FUNCTION_UNKNOWN(FORMULA, "231"),
+    FORMULA_VARIABLE_UNDEFINED(FORMULA, "241"),
+    /**
+     * Threshold must not be null.
+     */
+    THRESHOLD_NULL(WRONG_DEFINITION, "004"),
+    /**
+     * Variable must not be null.
+     */
+    VARIABLE_NAME_NULL(WRONG_DEFINITION, "007"),
+    /**
+     * Value for the variable must not be null.
+     */
+    VARIABLE_VALUE_NULL(COMPUTATION, "114");
 
     /**
      * Parent refers to the resource part.
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/type/ResourceErrorType.java b/src/main/java/fr/inrae/agroclim/indicators/exception/type/ResourceErrorType.java
index aef77fda..6e1f6074 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/exception/type/ResourceErrorType.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/type/ResourceErrorType.java
@@ -12,75 +12,76 @@ public enum ResourceErrorType implements ErrorType {
     /**
      * Climate, topic.
      */
-    CLIMATE("01", null, "resource.climatic"),
+    CLIMATE("100", null, "resource.climatic"),
     /**
      * No climate data.
      */
-    CLIMATE_EMPTY("02", CLIMATE, "empty"),
+    CLIMATE_EMPTY("110", CLIMATE, "empty"),
+    CLIMATE_EMPTY_FOR_PHASE("111", CLIMATE_EMPTY, "for.phase"),
     /**
      * Not enough data.
      */
-    CLIMATE_SIZE_WRONG("03", CLIMATE, "size.wrong"),
+    CLIMATE_SIZE_WRONG("101", CLIMATE, "size.wrong"),
     /**
      * Years of climate, topic.
      */
-    CLIMATIC_YEARS("04", null, "resource.climatic.years"),
+    CLIMATIC_YEARS("120", null, "resource.climatic.years"),
     /**
      * No years of climate.
      */
-    CLIMATE_YEARS_EMPTY("05", CLIMATIC_YEARS, "empty"),
+    CLIMATE_YEARS_EMPTY("121", CLIMATIC_YEARS, "empty"),
     /**
      * Not enough data.
      */
-    CLIMATE_YEARS_MISSING("06", CLIMATIC_YEARS, "missing"),
+    CLIMATE_YEARS_MISSING("122", CLIMATIC_YEARS, "missing"),
     /**
      * Phenology, topic.
      */
-    PHENO("07", null, "resource.pheno"),
+    PHENO("200", null, "resource.pheno"),
     /**
      * No phenological data.
      */
-    PHENO_EMPTY("08", PHENO, "empty"),
+    PHENO_EMPTY("201", PHENO, "empty"),
     /**
      * Years of phenology, topic.
      */
-    PHENO_YEARS("09", null, "resource.pheno.years"),
+    PHENO_YEARS("210", null, "resource.pheno.years"),
     /**
      * No years of phenology.
      */
-    PHENO_YEARS_EMPTY("10", PHENO_YEARS, "empty"),
+    PHENO_YEARS_EMPTY("211", PHENO_YEARS, "empty"),
     /**
      * Not enough data.
      */
-    PHENO_YEARS_MISSING("11", PHENO_YEARS, "missing"),
+    PHENO_YEARS_MISSING("212", PHENO_YEARS, "missing"),
     /**
      * Resource in general, topic.
      */
-    RESOURCE("12", null, "resource"),
+    RESOURCE("001", null, "resource"),
     /**
      * Setting not set.
      */
-    RESOURCE_CROPDEVELOPMENT_YEARS("13", RESOURCE, "cropdevelopmentyears.null"),
+    RESOURCE_CROPDEVELOPMENT_YEARS("002", RESOURCE, "cropdevelopmentyears.null"),
     /**
      * Soil, topic.
      */
-    SOIL("14", null, "resource.soil"),
+    SOIL("300", null, "resource.soil"),
     /**
      * Not enough data.
      */
-    SOIL_SIZE_WRONG("15", SOIL, "size.wrong"),
+    SOIL_SIZE_WRONG("301", SOIL, "size.wrong"),
     /**
      * Variables, topic.
      */
-    VARIABLES("16", null, "resource.variables"),
+    VARIABLES("400", null, "resource.variables"),
     /**
      * No variable.
      */
-    VARIABLES_EMPTY("17", VARIABLES, "empty"),
+    VARIABLES_EMPTY("401", VARIABLES, "empty"),
     /**
      * No variale.
      */
-    VARIABLES_MISSING("18", VARIABLES, "missing");
+    VARIABLES_MISSING("402", VARIABLES, "missing");
     /**
      * Key for Resource/I18nResource.
      */
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/Evaluation.java b/src/main/java/fr/inrae/agroclim/indicators/model/Evaluation.java
index 57d1497e..2648642c 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/Evaluation.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/Evaluation.java
@@ -26,14 +26,13 @@ import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Collectors;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
+import fr.inrae.agroclim.indicators.exception.type.ResourceErrorType;
 import fr.inrae.agroclim.indicators.model.data.ResourceManager;
 import fr.inrae.agroclim.indicators.model.data.Variable;
 import fr.inrae.agroclim.indicators.model.data.Variable.Type;
@@ -53,7 +52,6 @@ import fr.inrae.agroclim.indicators.model.indicator.listener.IndicatorEvent;
 import fr.inrae.agroclim.indicators.model.result.EvaluationResult;
 import fr.inrae.agroclim.indicators.model.result.IndicatorResult;
 import fr.inrae.agroclim.indicators.model.result.PhaseResult;
-import fr.inrae.agroclim.indicators.resources.I18n;
 import fr.inrae.agroclim.indicators.resources.Messages;
 import fr.inrae.agroclim.indicators.util.DateUtils;
 import fr.inrae.agroclim.indicators.util.StageUtils;
@@ -302,12 +300,10 @@ public final class Evaluation extends CompositeIndicator {
     /**
      * Compute indicator results.
      *
-     * @throws TechnicalException
-     *             from Indicator.compute()
-     * @throws FunctionalException
+     * @throws IndicatorsException
      *             from Indicator.compute()
      */
-    public void compute() throws TechnicalException, FunctionalException {
+    public void compute() throws IndicatorsException {
         LOGGER.trace("start computing evaluation \"" + getName() + "\"");
         fireIndicatorEvent(IndicatorEvent.Type.COMPUTE_START.event(this));
 
@@ -319,12 +315,10 @@ public final class Evaluation extends CompositeIndicator {
             throw new RuntimeException("Phase list is empty!");
         }
         if (resourceManager.getClimaticResource().isEmpty()) {
-            throw new FunctionalException(
-                    "There is not any climatic daily data!");
+            throw new IndicatorsException(ResourceErrorType.CLIMATE_EMPTY);
         }
         if (resourceManager.getPhenologicalResource().isEmpty()) {
-            throw new FunctionalException(
-                    "There is not any phenological data !");
+            throw new IndicatorsException(ResourceErrorType.PHENO_EMPTY);
         }
 
         clearResults();
@@ -408,26 +402,21 @@ public final class Evaluation extends CompositeIndicator {
                 final ClimaticResource climaticData = resourceManager
                         .getClimaticResource().getClimaticDataByPhaseAndYear(startDate, endDate);
                 if (climaticData.isEmpty()) {
-                    final I18n i18n = new I18n("fr.inrae.agroclim.indicators.resources.messages", Locale.getDefault());
-                    final StringBuilder sb = new StringBuilder();
-                    sb.append(i18n.format("error.climate.missing",
-                            startStageName, endStageName, startStage, endStage, dateYear));
                     if (resourceManager.getClimaticResource().isEmpty()) {
-                        sb.append(i18n.format("error.climate.no.data"));
-                        throw new RuntimeException(sb.toString());
-                    } else {
-                        final int yearToSearch = dateYear;
-                        final List<ClimaticDailyData> ddataList = resourceManager.getClimaticResource().getData()
-                                .stream().filter(f -> f.getYear() == yearToSearch).collect(Collectors.toList());
-
-                        final ClimaticDailyData startData = ddataList.get(0);
-                        final ClimaticDailyData endData = ddataList.get(ddataList.size() - 1);
-
-                        sb.append(i18n.format("error.climate.dates", dateYear,
-                                startData.getYear() + "-" + startData.getMonth() + "-" + startData.getDay(),
-                                endData.getYear() + "-" + endData.getMonth() + "-" + endData.getDay()));
+                        throw new IndicatorsException(ResourceErrorType.CLIMATE_EMPTY);
                     }
-                    throw new FunctionalException(sb.toString());
+                    final int yearToSearch = dateYear;
+                    final List<ClimaticDailyData> ddataList = resourceManager.getClimaticResource().getData()
+                            .stream().filter(f -> f.getYear() == yearToSearch).collect(Collectors.toList());
+
+                    final ClimaticDailyData startData = ddataList.get(0);
+                    final ClimaticDailyData endData = ddataList.get(ddataList.size() - 1);
+
+                    throw new IndicatorsException(ResourceErrorType.CLIMATE_EMPTY_FOR_PHASE,
+                            startStageName, endStageName, startStage, endStage, dateYear, //
+                            startData.getYear() + "-" + startData.getMonth() + "-" + startData.getDay(), //
+                            endData.getYear() + "-" + endData.getMonth() + "-" + endData.getDay()
+                    );
                 }
                 /* #9451 - En cas de données manquantes, on passe à l'année suivante
                  * Par défaut, le résultat de l'évaluation du couple phase/année vaut NA (null).
@@ -459,9 +448,9 @@ public final class Evaluation extends CompositeIndicator {
      * Pour chaque année, Pour chaque phase, valeurs.onIndicatorAdd(valeur phase
      * p, année n); Fin pour aggregation(valeurs); Fin pour;
      *
-     * @throws FunctionalException raised by AggregationFunction.aggregate()
+     * @throws IndicatorsException raised by AggregationFunction.aggregate()
      */
-    private void computeFaisability() throws FunctionalException {
+    private void computeFaisability() throws IndicatorsException {
         LOGGER.traceEntry();
         if (getType() == EvaluationType.WITHOUT_AGGREGATION) {
             return;
@@ -831,7 +820,7 @@ public final class Evaluation extends CompositeIndicator {
         getPhases().forEach(phase -> values.put(phase.getId(), 1.));
         try {
             getAggregationFunction().aggregate(values);
-        } catch (final FunctionalException ex) {
+        } catch (final IndicatorsException ex) {
             LOGGER.info("Invalid aggregation: {}", ex.getLocalizedMessage());
             return false;
         }
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/JEXLFormula.java b/src/main/java/fr/inrae/agroclim/indicators/model/JEXLFormula.java
index c3c52afe..98c5b2be 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/JEXLFormula.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/JEXLFormula.java
@@ -35,7 +35,8 @@ import org.apache.commons.jexl3.JexlFeatures;
 import org.apache.commons.jexl3.JexlScript;
 import org.apache.commons.jexl3.MapContext;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
+import fr.inrae.agroclim.indicators.exception.type.ComputationErrorType;
 import fr.inrae.agroclim.indicators.model.criteria.FormulaCriteria;
 import fr.inrae.agroclim.indicators.model.data.Variable;
 import fr.inrae.agroclim.indicators.model.function.aggregation.MathMethod;
@@ -119,15 +120,15 @@ public class JEXLFormula {
      * @param values variable name ⮕ value
      * @param clazz result class
      * @return The result of this evaluation
-     * @throws fr.inrae.agroclim.indicators.exception.FunctionalException on any error
+     * @throws IndicatorsException on any error
      */
     @SuppressWarnings("unchecked")
-    public <T> T evaluate(final Map<String, Double> values, final Class<T> clazz) throws FunctionalException {
+    public <T> T evaluate(final Map<String, Double> values, final Class<T> clazz) throws IndicatorsException {
         try {
             if (getExpression() == null) {
-                throw new FunctionalException(errorMessage("expression.null"));
-            } else if (getExpression().isEmpty()) {
-                throw new FunctionalException(errorMessage("expression.empty"));
+                throw new IndicatorsException(ComputationErrorType.FORMULA_EXPRESSION_NULL);
+            } else if (getExpression().isBlank()) {
+                throw new IndicatorsException(ComputationErrorType.FORMULA_EXPRESSION_BLANK);
             }
 
             final JexlExpression exp = jexl.createExpression(getExpression());
@@ -141,18 +142,18 @@ public class JEXLFormula {
                 context.set(text, values.get(key));
             });
             return (T) exp.evaluate(context);
-        } catch (final JexlException.Variable variable) {
-            throw new FunctionalException(errorMessage("variable.undefined", getExpression(), variable.getVariable()),
-                    variable);
-        } catch (final JexlException.Method method) {
-            throw new FunctionalException(errorMessage("function.unknown", getExpression(), method.getMethod()),
-                    method);
-        } catch (final JexlException.Parsing parsing) {
-            throw new FunctionalException(errorMessage("expression.parsing", getExpression()), parsing);
-        } catch (final JexlException.Tokenization token) {
-            throw new FunctionalException(errorMessage("expression.parenthesis", getExpression()), token);
-        } catch (final JexlException e) {
-            throw new FunctionalException(errorMessage("execution", getExpression()) + e.getLocalizedMessage(), e);
+        } catch (final JexlException.Variable ex) {
+            throw new IndicatorsException(ComputationErrorType.FORMULA_VARIABLE_UNDEFINED, ex, getExpression(),
+                    ex.getVariable());
+        } catch (final JexlException.Method ex) {
+            throw new IndicatorsException(ComputationErrorType.FORMULA_FUNCTION_UNKNOWN, ex, getExpression(),
+                    ex.getMethod());
+        } catch (final JexlException.Parsing ex) {
+            throw new IndicatorsException(ComputationErrorType.FORMULA_EXPRESSION_PARSING, ex, getExpression());
+        } catch (final JexlException.Tokenization ex) {
+            throw new IndicatorsException(ComputationErrorType.FORMULA_EXPRESSION_PARENTHESIS, ex, getExpression());
+        } catch (final JexlException ex) {
+            throw new IndicatorsException(ComputationErrorType.FORMULA, ex, getExpression());
         }
     }
 
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/Quantifiable.java b/src/main/java/fr/inrae/agroclim/indicators/model/Quantifiable.java
index 44472bdd..6eca2881 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/Quantifiable.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/Quantifiable.java
@@ -16,8 +16,7 @@
  */
 package fr.inrae.agroclim.indicators.model;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.data.DailyData;
 import fr.inrae.agroclim.indicators.model.data.Resource;
 
@@ -34,11 +33,7 @@ public interface Quantifiable {
      * @param data
      *            resource of daily data
      * @return single result for resource of daily data
-     * @throws TechnicalException
-     *             technical exception
-     * @throws FunctionalException
-     *             functional exception
+     * @throws IndicatorsException
      */
-    Double compute(Resource<? extends DailyData> data)
-            throws TechnicalException, FunctionalException;
+    Double compute(Resource<? extends DailyData> data) throws IndicatorsException;
 }
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/criteria/ComparisonCriteria.java b/src/main/java/fr/inrae/agroclim/indicators/model/criteria/ComparisonCriteria.java
index 453b4055..5779acb8 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/criteria/ComparisonCriteria.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/criteria/ComparisonCriteria.java
@@ -18,7 +18,8 @@
  */
 package fr.inrae.agroclim.indicators.model.criteria;
 
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
+import fr.inrae.agroclim.indicators.exception.type.ComputationErrorType;
 import fr.inrae.agroclim.indicators.model.Parameter;
 import fr.inrae.agroclim.indicators.model.data.DailyData;
 import fr.inrae.agroclim.indicators.model.data.Resource;
@@ -99,9 +100,10 @@ public final class ComparisonCriteria extends Criteria {
     }
 
     @Override
-    public boolean eval(final DailyData dailydata) throws TechnicalException {
+    public boolean eval(final DailyData dailydata) throws IndicatorsException {
         if (relationalOperator == null) {
-            throw new RuntimeException("The relational operator must be set!");
+            throw new IndicatorsException(ComputationErrorType.WRONG_DEFINITION,
+                    "The relational operator must be set!");
         }
         if (dailydata == null) {
             return false;
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/criteria/CompositeCriteria.java b/src/main/java/fr/inrae/agroclim/indicators/model/criteria/CompositeCriteria.java
index 37bb407f..3f09a9f9 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/criteria/CompositeCriteria.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/criteria/CompositeCriteria.java
@@ -16,12 +16,13 @@
  */
 package fr.inrae.agroclim.indicators.model.criteria;
 
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.type.ComputationErrorType;
 import fr.inrae.agroclim.indicators.model.Parameter;
 import fr.inrae.agroclim.indicators.model.data.DailyData;
 import fr.inrae.agroclim.indicators.model.data.Resource;
@@ -87,10 +88,9 @@ public final class CompositeCriteria extends Criteria {
     }
 
     @Override
-    public boolean eval(final DailyData dailyData)
-            throws TechnicalException {
+    public boolean eval(final DailyData dailyData) throws IndicatorsException {
         if (logicalOperator == null) {
-            throw new RuntimeException("The logical operator must be set!");
+            throw new IndicatorsException(ComputationErrorType.WRONG_DEFINITION, "The logical operator must be set!");
         }
         switch (logicalOperator) {
         case AND:
@@ -125,11 +125,10 @@ public final class CompositeCriteria extends Criteria {
      * @param data
      *            climatic data
      * @return formula
-     * @throws TechnicalException
+     * @throws IndicatorsException
      *             exception while getting data
      */
-    public String getFormula(final ClimaticDailyData data)
-            throws TechnicalException {
+    public String getFormula(final ClimaticDailyData data) throws IndicatorsException {
         StringBuilder sb = new StringBuilder();
         boolean first = true;
         for (final Criteria aCriteria : criteria) {
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/criteria/Criteria.java b/src/main/java/fr/inrae/agroclim/indicators/model/criteria/Criteria.java
index 4e368155..8934fd79 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/criteria/Criteria.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/criteria/Criteria.java
@@ -16,6 +16,7 @@
  */
 package fr.inrae.agroclim.indicators.model.criteria;
 
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import java.beans.PropertyChangeListener;
 import java.beans.PropertyChangeSupport;
 import java.io.Serializable;
@@ -28,7 +29,6 @@ import javax.xml.bind.annotation.XmlElementWrapper;
 import javax.xml.bind.annotation.XmlRootElement;
 import javax.xml.bind.annotation.XmlTransient;
 
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.Computable;
 import fr.inrae.agroclim.indicators.model.HasParameters;
 import fr.inrae.agroclim.indicators.model.Parameter;
@@ -114,11 +114,10 @@ Computable, HasParameters, Serializable, UseVariables {
      * @param data
      *            data to compare
      * @return comparison result
-     * @throws TechnicalException
+     * @throws IndicatorsException
      *             exception during evaluation
      */
-    public abstract boolean eval(DailyData data)
-            throws TechnicalException;
+    public abstract boolean eval(DailyData data) throws IndicatorsException;
 
     /**
      * @param indent
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/criteria/FormulaCriteria.java b/src/main/java/fr/inrae/agroclim/indicators/model/criteria/FormulaCriteria.java
index 29c23fd4..02c5a31d 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/criteria/FormulaCriteria.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/criteria/FormulaCriteria.java
@@ -24,15 +24,15 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Set;
 
 import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlRootElement;
 import javax.xml.bind.annotation.XmlType;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+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;
@@ -143,9 +143,13 @@ public final class FormulaCriteria extends Criteria {
     }
 
     @Override
-    public boolean eval(final DailyData data) throws TechnicalException {
-        Objects.requireNonNull(expression, "expression must not be null!");
-        Objects.requireNonNull(data, "Resource data must not be null!");
+    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)));
@@ -157,8 +161,8 @@ public final class FormulaCriteria extends Criteria {
         }
         try {
             return formula.evaluate(values, Boolean.class);
-        } catch (final FunctionalException ex) {
-            throw new TechnicalException("Failed to evaluate the criteria: " + ex.getLocalizedMessage(), ex);
+        } catch (final IndicatorsException ex) {
+            throw new IndicatorsException(ComputationErrorType.FORMULA, ex, "Failed to evaluate the criteria.");
         }
     }
 
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/criteria/NoCriteria.java b/src/main/java/fr/inrae/agroclim/indicators/model/criteria/NoCriteria.java
index 98660e25..d6c88efe 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/criteria/NoCriteria.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/criteria/NoCriteria.java
@@ -18,13 +18,13 @@
  */
 package fr.inrae.agroclim.indicators.model.criteria;
 
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import java.util.Map;
 
 import javax.xml.bind.annotation.XmlAccessType;
 import javax.xml.bind.annotation.XmlAccessorType;
 import javax.xml.bind.annotation.XmlType;
 
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.Parameter;
 import fr.inrae.agroclim.indicators.model.data.DailyData;
 import fr.inrae.agroclim.indicators.util.Doublet;
@@ -59,7 +59,7 @@ public final class NoCriteria extends VariableCriteria {
     }
 
     @Override
-    public boolean eval(final DailyData data) throws TechnicalException {
+    public boolean eval(final DailyData data) throws IndicatorsException {
         return true;
     }
 
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/criteria/SimpleCriteria.java b/src/main/java/fr/inrae/agroclim/indicators/model/criteria/SimpleCriteria.java
index 4c59c441..33703356 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/criteria/SimpleCriteria.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/criteria/SimpleCriteria.java
@@ -16,6 +16,7 @@
  */
 package fr.inrae.agroclim.indicators.model.criteria;
 
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
@@ -25,7 +26,6 @@ import javax.xml.bind.annotation.XmlRootElement;
 import javax.xml.bind.annotation.XmlTransient;
 import javax.xml.bind.annotation.XmlType;
 
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.Parameter;
 import fr.inrae.agroclim.indicators.model.data.DailyData;
 import fr.inrae.agroclim.indicators.model.data.Variable;
@@ -139,7 +139,7 @@ public final class SimpleCriteria extends VariableCriteria {
     }
 
     @Override
-    public boolean eval(final DailyData dailydata) throws TechnicalException {
+    public boolean eval(final DailyData dailydata) throws IndicatorsException {
         if (dailydata == null) {
             return false;
         }
@@ -168,11 +168,10 @@ public final class SimpleCriteria extends VariableCriteria {
      * @param data
      *            climatic data
      * @return formula
-     * @throws TechnicalException
+     * @throws IndicatorsException
      *             exception while getting data
      */
-    public String getFormula(final ClimaticDailyData data)
-            throws TechnicalException {
+    public String getFormula(final ClimaticDailyData data) throws IndicatorsException {
         final StringBuilder sb = new StringBuilder();
         sb.append(getValueOf(data));
         sb.append(" ");
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/criteria/VariableCriteria.java b/src/main/java/fr/inrae/agroclim/indicators/model/criteria/VariableCriteria.java
index 0f9c14a5..5e58c4f8 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/criteria/VariableCriteria.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/criteria/VariableCriteria.java
@@ -18,7 +18,8 @@
  */
 package fr.inrae.agroclim.indicators.model.criteria;
 
-import java.util.HashSet;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
+import fr.inrae.agroclim.indicators.exception.type.ComputationErrorType;
 import java.util.Objects;
 import java.util.Set;
 
@@ -67,25 +68,25 @@ public abstract class VariableCriteria extends Criteria {
      *
      * @param data climatic data
      * @return value of "variable"
+     * @throws IndicatorsException in case of wrong definition or while getting value
      */
-    public final double getValueOf(final DailyData data) {
+    public final double getValueOf(final DailyData data) throws IndicatorsException {
         if (data == null) {
-            throw new IllegalArgumentException("DailyData is null!");
+            throw new IndicatorsException(ComputationErrorType.DATA_NULL);
         }
         if (variable == null) {
-            throw new NullPointerException("variable is null!");
+            throw new IndicatorsException(ComputationErrorType.VARIABLE_NAME_NULL);
         }
-        if (data.getValue(variable) == null) {
-            throw new NullPointerException("At " + data.getDate() + ", " + variable + " is null!");
+        final var value = data.getValue(variable);
+        if (value == null) {
+            throw new IndicatorsException(ComputationErrorType.VARIABLE_VALUE_NULL, data.getDate(), variable);
         }
-        return data.getValue(variable);
+        return value;
     }
 
     @Override
     public final Set<Variable> getVariables() {
-        final Set<Variable> variables = new HashSet<>();
-        variables.add(variable);
-        return variables;
+        return Set.of(variable);
     }
 
     /**
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/function/aggregation/AggregationFunction.java b/src/main/java/fr/inrae/agroclim/indicators/model/function/aggregation/AggregationFunction.java
index 3498f290..0816bf08 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/function/aggregation/AggregationFunction.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/function/aggregation/AggregationFunction.java
@@ -23,7 +23,7 @@ import javax.xml.bind.annotation.XmlAccessType;
 import javax.xml.bind.annotation.XmlAccessorType;
 import javax.xml.bind.annotation.XmlAttribute;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.Setter;
@@ -56,11 +56,10 @@ public abstract class AggregationFunction implements Serializable {
      * @param values
      *            values to use for aggregation
      * @return aggregated value
-     * @throws FunctionalException
+     * @throws IndicatorsException
      *             exception
      */
-    public abstract double aggregate(Map<String, Double> values)
-            throws FunctionalException;
+    public abstract double aggregate(Map<String, Double> values) throws IndicatorsException;
 
     /**
      * @return function is valid
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/function/aggregation/JEXLFunction.java b/src/main/java/fr/inrae/agroclim/indicators/model/function/aggregation/JEXLFunction.java
index 498d0ee3..1e14519f 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/function/aggregation/JEXLFunction.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/function/aggregation/JEXLFunction.java
@@ -6,7 +6,7 @@ import java.util.Map;
 
 import javax.xml.bind.annotation.XmlRootElement;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.JEXLFormula;
 import lombok.EqualsAndHashCode;
 
@@ -41,7 +41,7 @@ public final class JEXLFunction extends AggregationFunction {
     }
 
     @Override
-    public double aggregate(final Map<String, Double> values) throws FunctionalException {
+    public double aggregate(final Map<String, Double> values) throws IndicatorsException {
         formula.setExpression(getExpression());
         return formula.evaluate(values, Double.class);
     }
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/AggregationIndicator.java b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/AggregationIndicator.java
index e48ad253..c64f4b32 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/AggregationIndicator.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/AggregationIndicator.java
@@ -7,9 +7,8 @@ import java.util.stream.DoubleStream;
 
 import javax.xml.bind.annotation.XmlElement;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
 import fr.inrae.agroclim.indicators.exception.IndicatorsException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.ThrowingToDoubleFunction;
 import fr.inrae.agroclim.indicators.exception.type.ComputationErrorType;
 import fr.inrae.agroclim.indicators.model.criteria.Criteria;
 import fr.inrae.agroclim.indicators.model.criteria.NoCriteria;
@@ -66,29 +65,36 @@ public abstract class AggregationIndicator extends SimpleIndicatorWithCriteria i
     }
 
     @Override
-    public final double computeSingleValue(final Resource<? extends DailyData> res)
-            throws TechnicalException, FunctionalException {
+    public final double computeSingleValue(final Resource<? extends DailyData> res) throws IndicatorsException {
         final DoubleStream stream;
-        if (getCriteria() instanceof final NoCriteria criteria) {
-            Objects.requireNonNull(criteria.getVariable(),
-                    getId() + ".getCriteria().getVariable() must not be null! " + getVariable());
-            // syntax changed to hack Eclipse.
-            stream = res.getData().stream().mapToDouble(d -> criteria.getValueOf(d));
-        } else if (getCriteria() instanceof final SimpleCriteria criteria) {
-            final Predicate<DailyData> predicate = d -> {
-                try {
-                    return criteria.eval(d);
-                } catch (final TechnicalException e) {
-                    return false;
-                }
-            };
-            stream = res.getData().stream() //
-                    .filter(predicate) //
-                    .mapToDouble(criteria::getValueOf);
-        } else {
-            throw new IndicatorsException(ComputationErrorType.CRITERIA_ISNT_NOCRITERIA_SIMPLECRITERIA, getId());
+        try {
+            if (getCriteria() instanceof final NoCriteria criteria) {
+                Objects.requireNonNull(criteria.getVariable(),
+                        getId() + ".getCriteria().getVariable() must not be null! " + getVariable());
+                // syntax changed to hack Eclipse.
+                stream = res.getData().stream() //
+                        .mapToDouble(ThrowingToDoubleFunction.wrap(criteria::getValueOf));
+            } else if (getCriteria() instanceof final SimpleCriteria criteria) {
+                final Predicate<DailyData> predicate = d -> {
+                    try {
+                        return criteria.eval(d);
+                    } catch (final IndicatorsException e) {
+                        return false;
+                    }
+                };
+                stream = res.getData().stream() //
+                        .filter(predicate) //
+                        .mapToDouble(ThrowingToDoubleFunction.wrap(criteria::getValueOf));
+            } else {
+                throw new IndicatorsException(ComputationErrorType.CRITERIA_ISNT_NOCRITERIA_SIMPLECRITERIA, getId());
+            }
+            return aggregate(stream);
+        } catch (final RuntimeException ex) {
+            if (ex.getCause() instanceof IndicatorsException iex) {
+                throw new IndicatorsException(ComputationErrorType.COMPUTATION, iex);
+            }
+            throw ex;
         }
-        return aggregate(stream);
     }
 
     @Override
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/AverageOfDiff.java b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/AverageOfDiff.java
index 469c6316..6b5a1dde 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/AverageOfDiff.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/AverageOfDiff.java
@@ -16,14 +16,13 @@
  */
 package fr.inrae.agroclim.indicators.model.indicator;
 
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import java.util.HashSet;
 import java.util.Set;
 
 import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlType;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.data.DailyData;
 import fr.inrae.agroclim.indicators.model.data.Resource;
 import fr.inrae.agroclim.indicators.model.data.Variable;
@@ -73,8 +72,7 @@ implements Detailable {
     }
 
     @Override
-    public double computeSingleValue(final Resource<? extends DailyData> res)
-            throws TechnicalException, FunctionalException {
+    public double computeSingleValue(final Resource<? extends DailyData> res) throws IndicatorsException {
         double value = 0;
         for (final DailyData data : res.getData()) {
             value += data.getValue(variable1)
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/CompositeIndicator.java b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/CompositeIndicator.java
index 4d395185..1de20057 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/CompositeIndicator.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/CompositeIndicator.java
@@ -30,8 +30,8 @@ import javax.xml.bind.annotation.XmlRootElement;
 import javax.xml.bind.annotation.XmlTransient;
 import javax.xml.bind.annotation.XmlType;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
+import fr.inrae.agroclim.indicators.exception.type.ComputationErrorType;
 import fr.inrae.agroclim.indicators.model.Evaluation;
 import fr.inrae.agroclim.indicators.model.EvaluationType;
 import fr.inrae.agroclim.indicators.model.Knowledge;
@@ -252,8 +252,7 @@ DataLoadingListener, Detailable, HasDataLoadingListener, Comparable<Indicator> {
     }
 
     @Override
-    public final Double compute(final Resource<? extends DailyData> climRessource)
-            throws TechnicalException, FunctionalException {
+    public final Double compute(final Resource<? extends DailyData> climRessource) throws IndicatorsException {
         if (climRessource.getYears().isEmpty()) {
             throw new RuntimeException(
                     String.format(
@@ -277,8 +276,8 @@ DataLoadingListener, Detailable, HasDataLoadingListener, Comparable<Indicator> {
             try {
                 final double value = indicator.compute(climRessource);
                 results.put(indicator.getId(), value);
-            } catch (FunctionalException | TechnicalException e) {
-                LOGGER.error("Failed to compute indicator '{}': {}", indicator.getId(), e.getMessage());
+            } catch (final IndicatorsException e) {
+                throw new IndicatorsException(ComputationErrorType.COMPOSITE_COMPUTATION, e, indicator.getId());
             }
         }
         if (isAggregationNeeded()) {
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/DayOfYear.java b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/DayOfYear.java
index 457b4229..8a5dcb18 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/DayOfYear.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/DayOfYear.java
@@ -20,8 +20,8 @@ import javax.xml.bind.annotation.XmlAccessType;
 import javax.xml.bind.annotation.XmlAccessorType;
 import javax.xml.bind.annotation.XmlRootElement;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
+import fr.inrae.agroclim.indicators.exception.type.ComputationErrorType;
 import fr.inrae.agroclim.indicators.model.data.DailyData;
 import fr.inrae.agroclim.indicators.model.data.Resource;
 import lombok.Getter;
@@ -73,10 +73,9 @@ implements Detailable {
     }
 
     @Override
-    public double computeSingleValue(final Resource<? extends DailyData> data)
-            throws TechnicalException, FunctionalException {
+    public double computeSingleValue(final Resource<? extends DailyData> data) throws IndicatorsException {
         if (first == null) {
-            throw new FunctionalException("first must not be null!");
+            throw new IndicatorsException(ComputationErrorType.WRONG_DEFINITION, "first must not be null");
         }
         double res = 0;
         boolean resultCriteria;
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/DiffOfSum.java b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/DiffOfSum.java
index 9b7418e3..e0468ca6 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/DiffOfSum.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/DiffOfSum.java
@@ -26,8 +26,7 @@ import java.util.Set;
 import javax.xml.bind.annotation.XmlRootElement;
 import javax.xml.bind.annotation.XmlType;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.Knowledge;
 import fr.inrae.agroclim.indicators.model.Parameter;
 import fr.inrae.agroclim.indicators.model.data.DailyData;
@@ -92,8 +91,7 @@ public final class DiffOfSum extends SimpleIndicator implements Detailable, Indi
     }
 
     @Override
-    public double computeSingleValue(final Resource<? extends DailyData> res)
-            throws TechnicalException, FunctionalException {
+    public double computeSingleValue(final Resource<? extends DailyData> res) throws IndicatorsException {
         final double sum1 = getSumVariable1().compute(res);
         final double sum2 = getSumVariable2().compute(res);
         return sum1 - sum2;
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/Formula.java b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/Formula.java
index 41b36101..bad3df9f 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/Formula.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/Formula.java
@@ -31,8 +31,9 @@ import java.util.stream.DoubleStream;
 
 import javax.xml.bind.annotation.XmlType;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+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.Knowledge;
@@ -134,11 +135,14 @@ public final class Formula extends SimpleIndicator implements Detailable {
     private transient Map<String, Double> parametersValues = new HashMap<>();
 
     @Override
-    public double computeSingleValue(final Resource<? extends DailyData> res)
-            throws TechnicalException, FunctionalException {
+    public double computeSingleValue(final Resource<? extends DailyData> res) throws IndicatorsException {
+        if (aggregation == null) {
+            throw new IndicatorsException(ComputationErrorType.FORMULA_AGGREGATION_NULL);
+        }
         Objects.requireNonNull(res, "Resource must not be null!");
-        Objects.requireNonNull(res.getData(), "Resource data must not be null!");
-        Objects.requireNonNull(aggregation, "aggregation must not be null!");
+        if (res.getData() == null) {
+            throw new IndicatorsException(ResourceErrorType.CLIMATE_EMPTY);
+        }
         final Set<Variable> dataVariables = getVariables();
         final List<Double> computedValues = new ArrayList<>();
         for (final DailyData data : res.getData()) {
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/Frequency.java b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/Frequency.java
index 2fc65a2d..30d8e437 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/Frequency.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/Frequency.java
@@ -22,8 +22,7 @@ import java.util.Set;
 
 import javax.xml.bind.annotation.XmlRootElement;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.Knowledge;
 import fr.inrae.agroclim.indicators.model.Parameter;
 import fr.inrae.agroclim.indicators.model.data.DailyData;
@@ -71,8 +70,7 @@ public final class Frequency extends SimpleIndicator implements Detailable {
     }
 
     @Override
-    public double computeSingleValue(final Resource<? extends DailyData> data)
-            throws TechnicalException, FunctionalException {
+    public double computeSingleValue(final Resource<? extends DailyData> data) throws IndicatorsException {
         final double cent = 100.;
         final int days = data.getData().size();
         return numberOfDays.compute(data) * cent / days;
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/InjectedParameter.java b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/InjectedParameter.java
index 778617d9..8ac93282 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/InjectedParameter.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/InjectedParameter.java
@@ -30,8 +30,7 @@ import javax.xml.bind.annotation.XmlAccessorType;
 import javax.xml.bind.annotation.XmlRootElement;
 import javax.xml.bind.annotation.XmlType;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.Knowledge;
 import fr.inrae.agroclim.indicators.model.Parameter;
 import fr.inrae.agroclim.indicators.model.data.DailyData;
@@ -77,8 +76,7 @@ public final class InjectedParameter extends SimpleIndicator {
     private Double parameterValue;
 
     @Override
-    public double computeSingleValue(final Resource<? extends DailyData> data)
-            throws TechnicalException, FunctionalException {
+    public double computeSingleValue(final Resource<? extends DailyData> data) throws IndicatorsException {
         if (parameterValue == null) {
             return 0;
         }
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/MaxWaveLength.java b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/MaxWaveLength.java
index fa816879..dca5dd74 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/MaxWaveLength.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/MaxWaveLength.java
@@ -20,8 +20,8 @@ import javax.xml.bind.annotation.XmlAccessType;
 import javax.xml.bind.annotation.XmlAccessorType;
 import javax.xml.bind.annotation.XmlRootElement;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
+import fr.inrae.agroclim.indicators.exception.type.ComputationErrorType;
 import fr.inrae.agroclim.indicators.model.data.DailyData;
 import fr.inrae.agroclim.indicators.model.data.Resource;
 import lombok.EqualsAndHashCode;
@@ -67,13 +67,12 @@ public final class MaxWaveLength extends SimpleIndicatorWithCriteria implements
     }
 
     @Override
-    public double computeSingleValue(final Resource<? extends DailyData> climatic)
-            throws TechnicalException, FunctionalException {
+    public double computeSingleValue(final Resource<? extends DailyData> climatic) throws IndicatorsException {
         if (getCriteria() == null) {
-            throw new RuntimeException("criteria must not be null!");
+            throw new IndicatorsException(ComputationErrorType.CRITERIA_NULL);
         }
         if (threshold == null) {
-            throw new RuntimeException("threshold must not be null!");
+            throw new IndicatorsException(ComputationErrorType.THRESHOLD_NULL);
         }
         int waveLength = 0;
         int max = 0;
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/NumberOfDays.java b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/NumberOfDays.java
index 048102c8..9ffa5fbf 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/NumberOfDays.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/NumberOfDays.java
@@ -20,8 +20,7 @@ import javax.xml.bind.annotation.XmlAccessType;
 import javax.xml.bind.annotation.XmlAccessorType;
 import javax.xml.bind.annotation.XmlRootElement;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.data.DailyData;
 import fr.inrae.agroclim.indicators.model.data.Resource;
 
@@ -57,8 +56,7 @@ public final class NumberOfDays extends SimpleIndicatorWithCriteria implements D
     }
 
     @Override
-    public double computeSingleValue(final Resource<? extends DailyData> res)
-            throws TechnicalException, FunctionalException {
+    public double computeSingleValue(final Resource<? extends DailyData> res) throws IndicatorsException {
         if (getCriteria() == null) {
             return res.getData().size();
         }
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/NumberOfWaves.java b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/NumberOfWaves.java
index 06b8074f..24ce21fa 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/NumberOfWaves.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/NumberOfWaves.java
@@ -20,8 +20,8 @@ import javax.xml.bind.annotation.XmlAccessType;
 import javax.xml.bind.annotation.XmlAccessorType;
 import javax.xml.bind.annotation.XmlElement;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
+import fr.inrae.agroclim.indicators.exception.type.ComputationErrorType;
 import fr.inrae.agroclim.indicators.model.Parameter;
 import fr.inrae.agroclim.indicators.model.data.DailyData;
 import fr.inrae.agroclim.indicators.model.data.Resource;
@@ -77,13 +77,12 @@ public final class NumberOfWaves extends SimpleIndicatorWithCriteria implements
     }
 
     @Override
-    public double computeSingleValue(final Resource<? extends DailyData> climatic)
-            throws TechnicalException, FunctionalException {
+    public double computeSingleValue(final Resource<? extends DailyData> climatic) throws IndicatorsException {
         if (getCriteria() == null) {
-            throw new RuntimeException("criteria must not be null!");
+            throw new IndicatorsException(ComputationErrorType.CRITERIA_NULL);
         }
         if (nbDays == null) {
-            throw new RuntimeException("nbDays must not be null!");
+            throw new IndicatorsException(ComputationErrorType.WRONG_DEFINITION, "nbDays must not be null!");
         }
         int index = 0;
         int nb = 0;
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/PhaseLength.java b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/PhaseLength.java
index 0347a871..12ace4a4 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/PhaseLength.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/PhaseLength.java
@@ -19,8 +19,7 @@ package fr.inrae.agroclim.indicators.model.indicator;
 import java.util.HashSet;
 import java.util.Set;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.data.DailyData;
 import fr.inrae.agroclim.indicators.model.data.Resource;
 import fr.inrae.agroclim.indicators.model.data.Variable;
@@ -50,8 +49,7 @@ implements Detailable {
     }
 
     @Override
-    public double computeSingleValue(final Resource<? extends DailyData> data)
-            throws TechnicalException, FunctionalException {
+    public double computeSingleValue(final Resource<? extends DailyData> data) throws IndicatorsException {
         return data.getData().size();
     }
 
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/PotentialSowingDaysFrequency.java b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/PotentialSowingDaysFrequency.java
index ffc4ccc8..54dc714d 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/PotentialSowingDaysFrequency.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/PotentialSowingDaysFrequency.java
@@ -24,8 +24,7 @@ import java.util.Set;
 
 import javax.xml.bind.annotation.XmlType;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.Knowledge;
 import fr.inrae.agroclim.indicators.model.Parameter;
 import fr.inrae.agroclim.indicators.model.data.DailyData;
@@ -35,6 +34,7 @@ import fr.inrae.agroclim.indicators.model.data.climate.ClimaticDailyData;
 import fr.inrae.agroclim.indicators.model.data.climate.ClimaticResource;
 import fr.inrae.agroclim.indicators.util.Doublet;
 import java.util.ArrayList;
+import java.util.Objects;
 import java.util.Optional;
 import lombok.Getter;
 import lombok.Setter;
@@ -158,12 +158,8 @@ public final class PotentialSowingDaysFrequency extends SimpleIndicator implemen
     }
 
     @Override
-    public double computeSingleValue(
-            final Resource<? extends DailyData> resource)
-                    throws TechnicalException, FunctionalException {
-        if (resource == null) {
-            throw new IllegalArgumentException("resource must not be null!");
-        }
+    public double computeSingleValue(final Resource<? extends DailyData> resource) throws IndicatorsException {
+        Objects.requireNonNull(resource, "Resource must not be null!");
         if (!(resource instanceof ClimaticResource)) {
             throw new IllegalArgumentException(
                     getClass().getName()
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/Quotient.java b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/Quotient.java
index b3d9e520..87ec9eef 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/Quotient.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/Quotient.java
@@ -28,8 +28,8 @@ import javax.xml.bind.annotation.XmlAccessorType;
 import javax.xml.bind.annotation.XmlRootElement;
 import javax.xml.bind.annotation.XmlType;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
+import fr.inrae.agroclim.indicators.exception.type.ComputationErrorType;
 import fr.inrae.agroclim.indicators.model.Knowledge;
 import fr.inrae.agroclim.indicators.model.Parameter;
 import fr.inrae.agroclim.indicators.model.data.DailyData;
@@ -93,35 +93,27 @@ public final class Quotient extends SimpleIndicator implements Detailable, Indic
     }
 
     @Override
-    public double computeSingleValue(final Resource<? extends DailyData> data)
-            throws TechnicalException, FunctionalException {
+    public double computeSingleValue(final Resource<? extends DailyData> data) throws IndicatorsException {
         if (dividend == null) {
-            throw new TechnicalException(
-                    "Dividend indicator should never be null! Check "
-                            + "this indicator " + getId());
+            throw new IndicatorsException(ComputationErrorType.QUOTIENT_DIVIDEND_NULL);
         }
         if (divisor == null) {
-            throw new TechnicalException(
-                    "Divisor indicator should never be null! Check "
-                            + "this indicator " + getId());
+            throw new IndicatorsException(ComputationErrorType.QUOTIENT_DIVISOR_NULL);
         }
         final double dividendValue;
         try {
             dividendValue = dividend.compute(data);
-        } catch (final NullPointerException e) {
-            throw new FunctionalException("Computed value of divident "
-                    + dividend.getId() + " is null", e);
+        } catch (final IndicatorsException e) {
+            throw new IndicatorsException(ComputationErrorType.QUOTIENT_DIVIDEND_EXCEPTION, e, dividend.getId());
         }
         final double divisorValue;
         try {
             divisorValue = divisor.compute(data);
-        } catch (final NullPointerException e) {
-            throw new FunctionalException("Computed value of divisor "
-                    + divisor.getId() + " is null", e);
+        } catch (final IndicatorsException e) {
+            throw new IndicatorsException(ComputationErrorType.QUOTIENT_DIVISOR_EXCEPTION, e, divisor.getId());
         }
         if (divisorValue == 0.0) {
-            throw new FunctionalException("Cannot compute value for '" + getId() + "' as value of divisor '"
-                    + divisor.getId() + "' is 0.");
+            throw new IndicatorsException(ComputationErrorType.QUOTIENT_DIVISOR_ZERO, divisor.getId());
         }
         return dividendValue / divisorValue;
     }
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/SimpleIndicator.java b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/SimpleIndicator.java
index 5afa03a7..f09ec130 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/SimpleIndicator.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/SimpleIndicator.java
@@ -18,8 +18,7 @@ package fr.inrae.agroclim.indicators.model.indicator;
 
 import javax.xml.bind.annotation.XmlSeeAlso;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.EvaluationType;
 import fr.inrae.agroclim.indicators.model.data.DailyData;
 import fr.inrae.agroclim.indicators.model.data.Resource;
@@ -73,19 +72,10 @@ public abstract class SimpleIndicator extends Indicator {
      * @param data
      *            climatic resource used to compute indicator.
      * @return normalized result
-     * @throws fr.inrae.agroclim.indicators.exception.TechnicalException
-     * @throws fr.inrae.agroclim.indicators.exception.FunctionalException
      */
     @Override
-    public final Double compute(final Resource<? extends DailyData> data)
-            throws TechnicalException, FunctionalException {
-        double value;
-        try {
-            value  = computeSingleValue(data);
-        } catch (TechnicalException | FunctionalException e) {
-            LOGGER.warn("Indicator '{}' failed to compute single value: {}.", getId(), e.getMessage());
-            throw e;
-        }
+    public final Double compute(final Resource<? extends DailyData> data) throws IndicatorsException {
+        double value = computeSingleValue(data);
         setNotNormalizedValue(value);
         if (getNormalizationFunction() != null
                 && getType() != EvaluationType.WITHOUT_AGGREGATION) {
@@ -101,13 +91,10 @@ public abstract class SimpleIndicator extends Indicator {
      * @param data
      *            resource with daily data
      * @return value
-     * @throws TechnicalException
-     *             exception
-     * @throws FunctionalException
+     * @throws IndicatorsException
      *             exception
      */
-    public abstract double computeSingleValue(Resource<? extends DailyData> data)
-                    throws TechnicalException, FunctionalException;
+    public abstract double computeSingleValue(Resource<? extends DailyData> data) throws IndicatorsException;
 
     @Override
     public final void fireValueUpdated() {
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/Tamm.java b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/Tamm.java
index d0904fbb..a7bf8a88 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/Tamm.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/Tamm.java
@@ -25,8 +25,7 @@ import java.util.Set;
 
 import javax.xml.bind.annotation.XmlType;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.Knowledge;
 import fr.inrae.agroclim.indicators.model.Parameter;
 import fr.inrae.agroclim.indicators.model.data.DailyData;
@@ -154,8 +153,7 @@ public final class Tamm extends SimpleIndicator implements Detailable {
     }
 
     @Override
-    public double computeSingleValue(final Resource<? extends DailyData> data)
-            throws TechnicalException, FunctionalException {
+    public double computeSingleValue(final Resource<? extends DailyData> data) throws IndicatorsException {
         return data.getData().stream() //
                 .map(this::computeDailyValue) //
                 .reduce((v1, v2) -> v1 + v2).orElseThrow();
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/CulturalPracticesTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/CulturalPracticesTest.java
index b869201a..55d2cedd 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/CulturalPracticesTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/CulturalPracticesTest.java
@@ -19,8 +19,7 @@
 
 package fr.inrae.agroclim.indicators.model;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.data.DataTestHelper;
 import java.io.File;
 import lombok.extern.log4j.Log4j2;
@@ -76,7 +75,7 @@ public class CulturalPracticesTest extends DataTestHelper {
             long start = System.currentTimeMillis();
             evaluation.compute();
             LOGGER.info("Evaluation.compute() last {} seconds", (System.currentTimeMillis() - start) / 1000.);
-        } catch (final TechnicalException | FunctionalException e) {
+        } catch (final IndicatorsException e) {
             LOGGER.catching(e);
             error = e.getClass() + " : " + e.getLocalizedMessage();
         }
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/EvaluationHourlyTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/EvaluationHourlyTest.java
index 10eb9d61..f90f64f0 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/EvaluationHourlyTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/EvaluationHourlyTest.java
@@ -12,8 +12,7 @@ import java.util.Map;
 import org.junit.Before;
 import org.junit.Test;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.data.DataTestHelper;
 import fr.inrae.agroclim.indicators.model.result.EvaluationResult;
 import fr.inrae.agroclim.indicators.model.result.IndicatorResult;
@@ -57,7 +56,7 @@ public class EvaluationHourlyTest extends DataTestHelper {
         try {
             evaluation.initializeResources();
             evaluation.compute();
-        } catch (final TechnicalException | FunctionalException e) {
+        } catch (final IndicatorsException e) {
             error = e.getClass() + " : " + e.getLocalizedMessage();
         }
         assertNull(error, error);
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/EvaluationRobertTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/EvaluationRobertTest.java
index 16aae0ed..e1a505c3 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/EvaluationRobertTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/EvaluationRobertTest.java
@@ -18,8 +18,7 @@
  */
 package fr.inrae.agroclim.indicators.model;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.data.DataTestHelper;
 import fr.inrae.agroclim.indicators.model.data.climate.ClimaticResource;
 import fr.inrae.agroclim.indicators.model.result.EvaluationResult;
@@ -82,7 +81,7 @@ public class EvaluationRobertTest extends DataTestHelper {
         try {
             evaluation.initializeResources();
             evaluation.compute();
-        } catch (final TechnicalException | FunctionalException e) {
+        } catch (final IndicatorsException e) {
             error = e.getClass() + " : " + e.getLocalizedMessage();
         }
         assertNull(error, error);
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/EvaluationTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/EvaluationTest.java
index a1d8b3e9..a7bf99dd 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/EvaluationTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/EvaluationTest.java
@@ -40,8 +40,7 @@ import java.util.stream.Collectors;
 import org.junit.Before;
 import org.junit.Test;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.data.DataTestHelper;
 import fr.inrae.agroclim.indicators.model.data.ResourceManager;
 import fr.inrae.agroclim.indicators.model.data.Variable;
@@ -68,7 +67,7 @@ public final class EvaluationTest extends DataTestHelper {
      * Evaluation from good XML file.
      */
     private Evaluation evaluation;
-    
+
     /**
      * Evaluation from good XML file and with a climatic file contains NA data.
      */
@@ -123,12 +122,12 @@ public final class EvaluationTest extends DataTestHelper {
         try {
             evaluation.initializeResources();
             evaluation.compute();
-        } catch (final TechnicalException | FunctionalException e) {
+        } catch (final IndicatorsException e) {
             error = e.getClass() + " : " + e.getLocalizedMessage();
         }
         assertNull(error, error);
     }
-    
+
     @Test
     public void computableWithNoData() {
     	final File xmlFile = getEvaluationWithNoDataTestFile();
@@ -139,11 +138,11 @@ public final class EvaluationTest extends DataTestHelper {
     	evaluationWithNoData.initializeResources();
         try {
         	evaluationWithNoData.compute();
-        } catch (final TechnicalException | FunctionalException e) {
+        } catch (final IndicatorsException e) {
             error = e.getClass() + " : " + e.getLocalizedMessage();
         }
         assertNull(error, error);
-        
+
     }
 
     /**
@@ -190,7 +189,7 @@ public final class EvaluationTest extends DataTestHelper {
             final int nbOfPhases = evaluation.getIndicators().size();
             final int nbOfYears = evaluation.getClimaticResource().getYears().size();
             assertEquals(nbOfPhases * nbOfYears, phases.size());
-        } catch (final TechnicalException | FunctionalException e) {
+        } catch (final IndicatorsException e) {
             error = e.getClass() + " : " + e.getLocalizedMessage();
         }
         assertNull(error, error);
@@ -260,7 +259,7 @@ public final class EvaluationTest extends DataTestHelper {
                     });
                 });
             });
-        } catch (final TechnicalException | FunctionalException e) {
+        } catch (final IndicatorsException e) {
             throw new RuntimeException("This should never occur.", e);
         }
         assertTrue(duplicates.toString(), duplicates.isEmpty());
@@ -424,7 +423,7 @@ public final class EvaluationTest extends DataTestHelper {
         try {
             evaluation.initializeResources();
             evaluation.compute();
-        } catch (final TechnicalException | FunctionalException e) {
+        } catch (final IndicatorsException e) {
             error = e.getClass() + " : " + e.getLocalizedMessage();
         }
         assertNull(error, error);
@@ -440,7 +439,7 @@ public final class EvaluationTest extends DataTestHelper {
         evaluation.setParametersValues(parameters);
         try {
             evaluation.compute();
-        } catch (TechnicalException | FunctionalException e) {
+        } catch (IndicatorsException e) {
             error = e.getClass() + " : " + e.getLocalizedMessage();
         }
         assertNull(error, error);
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/EvaluationWithoutAggregationTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/EvaluationWithoutAggregationTest.java
index 989ea677..e9a8794d 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/EvaluationWithoutAggregationTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/EvaluationWithoutAggregationTest.java
@@ -18,7 +18,7 @@
  */
 package fr.inrae.agroclim.indicators.model;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.data.DataTestHelper;
 import fr.inrae.agroclim.indicators.model.indicator.CompositeIndicator;
@@ -76,13 +76,10 @@ public class EvaluationWithoutAggregationTest extends DataTestHelper {
 
     /**
      * Check if evaluation computes.
-     * @throws fr.inrae.agroclim.indicators.exception.TechnicalException while
-     * computing
-     * @throws fr.inrae.agroclim.indicators.exception.FunctionalException while
-     * computing
+     * @throws IndicatorsException while computing
      */
     @Test(expected = Test.None.class)
-    public void compute() throws TechnicalException, FunctionalException {
+    public void compute() throws IndicatorsException {
     	assertNotNull("Evaluation must not be null!", evaluation);
         evaluation.initializeResources();
         evaluation.compute();
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/EvalutationCustomHeadersTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/EvalutationCustomHeadersTest.java
index f7bd5554..325eaa03 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/EvalutationCustomHeadersTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/EvalutationCustomHeadersTest.java
@@ -18,8 +18,7 @@
  */
 package fr.inrae.agroclim.indicators.model;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.data.DataTestHelper;
 import fr.inrae.agroclim.indicators.model.result.EvaluationResult;
 import java.io.File;
@@ -65,7 +64,7 @@ public class EvalutationCustomHeadersTest extends DataTestHelper {
         try {
             evaluation.initializeResources();
             evaluation.compute();
-        } catch (final TechnicalException | FunctionalException e) {
+        } catch (final IndicatorsException e) {
             error = e.getClass() + " : " + e.getLocalizedMessage();
         }
         assertNull(error, error);
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/JEXLFormulaTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/JEXLFormulaTest.java
index b80a7ffc..908876e5 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/JEXLFormulaTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/JEXLFormulaTest.java
@@ -18,7 +18,7 @@
  */
 package fr.inrae.agroclim.indicators.model;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.data.Variable;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -55,7 +55,7 @@ public class JEXLFormulaTest {
     }
 
     @Test
-    public void useFormulaCriteria() throws FunctionalException {
+    public void useFormulaCriteria() throws IndicatorsException {
         final JEXLFormula formula = new JEXLFormula();
         formula.setExpression("formulaCriteria:between(2, 1, 3)");
         var actual = formula.evaluate(new HashMap<>(), Boolean.class);
@@ -63,7 +63,7 @@ public class JEXLFormulaTest {
     }
 
     @Test
-    public void useMathMethod() throws FunctionalException {
+    public void useMathMethod() throws IndicatorsException {
         final JEXLFormula formula = new JEXLFormula();
         formula.setExpression("math:min(2, 1, 3)");
         var actual = formula.evaluate(new HashMap<>(), Double.class);
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/KnowledgeTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/KnowledgeTest.java
index 92ca75b2..3b44ce15 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/KnowledgeTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/KnowledgeTest.java
@@ -347,6 +347,7 @@ public final class KnowledgeTest {
     @Test
     public void noNullVariables() {
         getSimpleIndicators().forEach((ind) -> {
+            System.out.println("=> " + ind);
             assertNotNull(ind);
             final String id = ind.getId();
             final Set<Variable> vars = ind.getVariables();
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/MMarjouTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/MMarjouTest.java
index 34ef61c4..75ca9303 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/MMarjouTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/MMarjouTest.java
@@ -16,8 +16,7 @@
  */
 package fr.inrae.agroclim.indicators.model;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.data.DataTestHelper;
 import fr.inrae.agroclim.indicators.model.data.phenology.AnnualStageData;
 import fr.inrae.agroclim.indicators.model.result.EvaluationResult;
@@ -80,7 +79,7 @@ public class MMarjouTest extends DataTestHelper {
             long start = System.currentTimeMillis();
             evaluation.compute();
             LOGGER.info("Evaluation.compute() last {} seconds", (System.currentTimeMillis() - start) / 1000.);
-        } catch (final TechnicalException | FunctionalException e) {
+        } catch (final IndicatorsException e) {
             error = e.getClass() + " : " + e.getLocalizedMessage();
         }
         assertNull(error, error);
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/RaidayMeantTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/RaidayMeantTest.java
index 1579b2f2..ffacfcdc 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/RaidayMeantTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/RaidayMeantTest.java
@@ -23,7 +23,6 @@ import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import java.io.File;
-import java.io.IOException;
 import java.nio.file.Paths;
 import java.text.SimpleDateFormat;
 import java.util.Arrays;
@@ -37,7 +36,7 @@ import java.util.stream.IntStream;
 import org.junit.Before;
 import org.junit.Test;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.criteria.RelationalOperator;
 import fr.inrae.agroclim.indicators.model.criteria.SimpleCriteria;
@@ -57,6 +56,7 @@ import fr.inrae.agroclim.indicators.model.result.IndicatorResult;
 import fr.inrae.agroclim.indicators.model.result.PhaseResult;
 import fr.inrae.agroclim.indicators.util.DateUtils;
 import fr.inrae.agroclim.indicators.xml.XMLUtil;
+import java.io.IOException;
 import java.util.TimeZone;
 import lombok.NonNull;
 import lombok.extern.log4j.Log4j2;
@@ -224,7 +224,7 @@ public class RaidayMeantTest extends DataTestHelper {
             evaluation.initializeResources();
             evaluation.compute();
             annualStageDatas = evaluation.getResourceManager().getPhenologicalResource().getData();
-        } catch (final TechnicalException | FunctionalException | IOException e) {
+        } catch (final IndicatorsException | IOException e) {
             error = e.getClass() + " : " + e.getLocalizedMessage();
         }
         assertNull(error, error);
@@ -253,7 +253,7 @@ public class RaidayMeantTest extends DataTestHelper {
         try {
             evaluation.initializeResources();
             evaluation.compute();
-        } catch (final TechnicalException | FunctionalException e) {
+        } catch (final IndicatorsException e) {
             error = e.getClass() + " : " + e.getLocalizedMessage();
         }
         assertNull(error, error);
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/StageDeltaEvaluationTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/StageDeltaEvaluationTest.java
index e3743d58..38b9d376 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/StageDeltaEvaluationTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/StageDeltaEvaluationTest.java
@@ -27,7 +27,7 @@ import java.util.Map;
 import org.junit.Before;
 import org.junit.Test;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.data.DataTestHelper;
 import fr.inrae.agroclim.indicators.xml.XMLUtil;
@@ -92,7 +92,7 @@ public class StageDeltaEvaluationTest extends DataTestHelper {
             final long start = System.currentTimeMillis();
             evaluation.compute();
             LOGGER.info("Evaluation.compute() last {} seconds", (System.currentTimeMillis() - start) / 1000.);
-        } catch (final TechnicalException | FunctionalException e) {
+        } catch (final IndicatorsException e) {
             error = e.getClass() + " : " + e.getLocalizedMessage();
         }
         assertNull(error, error);
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/criteria/CompositeCriteriaTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/criteria/CompositeCriteriaTest.java
index 3b198104..d37518aa 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/criteria/CompositeCriteriaTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/criteria/CompositeCriteriaTest.java
@@ -16,6 +16,7 @@
  */
 package fr.inrae.agroclim.indicators.model.criteria;
 
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
@@ -24,7 +25,6 @@ import java.util.List;
 
 import org.junit.Test;
 
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.data.climate.ClimaticDailyData;
 
 /**
@@ -88,11 +88,11 @@ public final class CompositeCriteriaTest {
     /**
      * Check sup0 AND inf10 criteria.
      *
-     * @throws TechnicalException
+     * @throws IndicatorsException
      *             exception while getting value.
      */
     @Test
-    public void and() throws TechnicalException {
+    public void and() throws IndicatorsException {
         criteria.setLogicalOperator(LogicalOperator.AND);
         assertFalse(criteria.getFormula(data0) + " must be false",
                 criteria.eval(data0));
@@ -105,11 +105,11 @@ public final class CompositeCriteriaTest {
     /**
      * Check inf10 criteria.
      *
-     * @throws TechnicalException
+     * @throws IndicatorsException
      *             exception while getting value.
      */
     @Test
-    public void inf10() throws TechnicalException {
+    public void inf10() throws IndicatorsException {
         assertTrue(inf10.getFormula(data0) + " must be true",
                 inf10.eval(data0));
         assertTrue(inf10.getFormula(data5) + " must be true",
@@ -121,11 +121,11 @@ public final class CompositeCriteriaTest {
     /**
      * Check sup0 OR inf10 criteria.
      *
-     * @throws TechnicalException
+     * @throws IndicatorsException
      *             exception while getting value.
      */
     @Test
-    public void or() throws TechnicalException {
+    public void or() throws IndicatorsException {
         criteria.setLogicalOperator(LogicalOperator.OR);
         assertTrue(criteria.getFormula(data0) + " must be true",
                 criteria.eval(data0));
@@ -138,11 +138,11 @@ public final class CompositeCriteriaTest {
     /**
      * Check sup0 criteria.
      *
-     * @throws TechnicalException
+     * @throws IndicatorsException
      *             exception while getting value.
      */
     @Test
-    public void sup0() throws TechnicalException {
+    public void sup0() throws IndicatorsException {
         assertFalse(
                 sup0.getFormula(data0) + " must be false. " + sup0.toString(),
                 sup0.eval(data0));
@@ -154,11 +154,11 @@ public final class CompositeCriteriaTest {
     /**
      * Check sup0 XOR inf10 criteria.
      *
-     * @throws TechnicalException
+     * @throws IndicatorsException
      *             exception while getting value.
      */
     @Test
-    public void xor() throws TechnicalException {
+    public void xor() throws IndicatorsException {
         criteria.setLogicalOperator(LogicalOperator.XOR);
         assertTrue(criteria.getFormula(data0) + " must be true",
                 criteria.eval(data0));
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/criteria/FormulaCriteriaTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/criteria/FormulaCriteriaTest.java
index 53fa19e2..63820714 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/criteria/FormulaCriteriaTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/criteria/FormulaCriteriaTest.java
@@ -18,6 +18,7 @@
  */
 package fr.inrae.agroclim.indicators.model.criteria;
 
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -73,14 +74,14 @@ public class FormulaCriteriaTest {
     }
 
     @Test
-    public void formulaFunction() throws TechnicalException {
+    public void formulaFunction() throws IndicatorsException {
         final FormulaCriteria crit = new FormulaCriteria();
         crit.setExpression("formulaCriteria:between(TMEAN, 10, 20)");
         assertTrue(crit.eval(data));
     }
 
     @Test
-    public void formulaWithParameter() throws TechnicalException {
+    public void formulaWithParameter() throws IndicatorsException, TechnicalException {
         final FormulaCriteria crit = new FormulaCriteria();
         crit.setExpression("formulaCriteria:between(TMEAN, min, max)");
         final List<ExpressionParameter> expressionParameters = new ArrayList<>();
@@ -120,8 +121,8 @@ public class FormulaCriteriaTest {
         assertTrue(variables.contains(Variable.TMEAN));
     }
 
-    @Test(expected = TechnicalException.class)
-    public void wrongFunction() throws TechnicalException {
+    @Test(expected = IndicatorsException.class)
+    public void wrongFunction() throws IndicatorsException {
         final FormulaCriteria crit = new FormulaCriteria();
         crit.setExpression("that:notExists(TMEAN, 10, 20)");
         crit.eval(data);
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/criteria/SimpleCriteriaTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/criteria/SimpleCriteriaTest.java
index ca9a0d88..a74af17e 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/criteria/SimpleCriteriaTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/criteria/SimpleCriteriaTest.java
@@ -1,5 +1,6 @@
 package fr.inrae.agroclim.indicators.model.criteria;
 
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -48,7 +49,7 @@ public class SimpleCriteriaTest {
     }
 
     @Test
-    public void gt() throws TechnicalException {
+    public void gt() throws IndicatorsException {
         final double dek = 10.;
         final SimpleCriteria criteria = new SimpleCriteria();
         criteria.setOperator(RelationalOperator.GT);
@@ -59,7 +60,7 @@ public class SimpleCriteriaTest {
     }
 
     @Test
-    public void le() throws TechnicalException {
+    public void le() throws IndicatorsException {
         final double dek = 10.;
         final SimpleCriteria criteria = new SimpleCriteria();
         criteria.setOperator(RelationalOperator.LE);
@@ -70,7 +71,7 @@ public class SimpleCriteriaTest {
     }
 
     @Test
-    public void inferiorToThreshold() throws TechnicalException {
+    public void inferiorToThreshold() throws IndicatorsException {
         final double dek = 10.;
         final SimpleCriteria criteria = new SimpleCriteria();
         criteria.setInferiorToThreshold(true);
@@ -111,7 +112,7 @@ public class SimpleCriteriaTest {
     }
 
     @Test(expected = Test.None.class)
-    public void unserializeGT() throws TechnicalException {
+    public void unserializeGT() throws IndicatorsException, TechnicalException {
         final Class<?>[] classes = new Class<?>[]{ Criteria.class, SimpleCriteria.class };
         final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"
                 + "<criteria xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"simpleCriteria\">"
@@ -131,7 +132,7 @@ public class SimpleCriteriaTest {
     }
 
     @Test(expected = Test.None.class)
-    public void unserializeInferiorToThreshold() throws TechnicalException {
+    public void unserializeInferiorToThreshold() throws IndicatorsException, TechnicalException {
         final Class<?>[] classes = new Class<?>[]{ Criteria.class, SimpleCriteria.class };
         final String xml = """
                            <?xml version="1.0" encoding="UTF-8" ?><criteria xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="simpleCriteria">    <variable>th</variable>
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/function/aggregation/JEXLFunctionTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/function/aggregation/JEXLFunctionTest.java
index eedc6f65..5b226497 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/function/aggregation/JEXLFunctionTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/function/aggregation/JEXLFunctionTest.java
@@ -23,7 +23,7 @@ import java.util.HashMap;
 
 import org.junit.Test;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 
 /**
  * Test JEXLFunction.
@@ -52,12 +52,12 @@ public final class JEXLFunctionTest {
             final Double expected = a * MathMethod.min(b, 2.) - 10;
             final Double actual = func.aggregate(data);
             assertEquals(expected, actual);
-        } catch (final FunctionalException ex) {
-            error = ex.getMessage() + ex.getRootException();
+        } catch (final IndicatorsException ex) {
+            error = ex.getMessage();
         }
         assertNull(error);
     }
-    
+
     /**
      * Test expression with variable names starting with $.
      */
@@ -75,8 +75,8 @@ public final class JEXLFunctionTest {
             final Double expected = one * MathMethod.min(two, 2.) - 10;
             final Double actual = func.aggregate(data);
             assertEquals(expected, actual);
-        } catch (final FunctionalException ex) {
-            error = ex.getMessage() + ex.getRootException();
+        } catch (final IndicatorsException ex) {
+            error = ex.getMessage();
         }
         assertNull(error);
     }
@@ -99,8 +99,8 @@ public final class JEXLFunctionTest {
             final Double expected = one * MathMethod.min(two, 2) - 10;
             final Double actual = func.aggregate(data);
             assertEquals(expected, actual);
-        } catch (final FunctionalException ex) {
-            error = ex.getMessage() + ex.getRootException();
+        } catch (final IndicatorsException ex) {
+            error = ex.getMessage();
         }
         assertNull(error);
     }
@@ -119,8 +119,8 @@ public final class JEXLFunctionTest {
             final Double expected = 0.;
             assertEquals(expected, actual);
 
-        } catch (final FunctionalException ex) {
-            error = ex.getMessage() + ex.getRootException();
+        } catch (final IndicatorsException ex) {
+            error = ex.getMessage();
         }
         assertNull(error);
     }
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/AverageOfDiffTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/AverageOfDiffTest.java
index 790e6c59..7c73ef0c 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/AverageOfDiffTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/AverageOfDiffTest.java
@@ -18,8 +18,7 @@
  */
 package fr.inrae.agroclim.indicators.model.indicator;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.data.DataTestHelper;
 import fr.inrae.agroclim.indicators.model.data.Variable;
 import fr.inrae.agroclim.indicators.model.data.climate.ClimaticResource;
@@ -69,7 +68,7 @@ public class AverageOfDiffTest extends DataTestHelper {
         String error = null;
         try {
             result = indicator.computeSingleValue(climaticData);
-        } catch (TechnicalException | FunctionalException ex) {
+        } catch (final IndicatorsException ex) {
             error = ex.getClass().getCanonicalName() + " : " + ex.getMessage();
         }
         assertNull(error, error );
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/AverageTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/AverageTest.java
index 11db4ece..3014fe06 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/AverageTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/AverageTest.java
@@ -32,7 +32,7 @@ import java.nio.file.Path;
 
 import org.junit.Test;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.Knowledge;
 import fr.inrae.agroclim.indicators.model.TimeScale;
@@ -80,7 +80,7 @@ public class AverageTest extends DataTestHelper {
         String error = null;
         try {
             result = indicator.computeSingleValue(climaticData);
-        } catch (TechnicalException | FunctionalException ex) {
+        } catch (IndicatorsException ex) {
             error = ex.getClass().getCanonicalName() + " : " + ex.getMessage();
         }
         assertEquals(null, error);
@@ -94,7 +94,7 @@ public class AverageTest extends DataTestHelper {
     }
 
     @Test
-    public void mint() throws TechnicalException, FunctionalException, CloneNotSupportedException {
+    public void mint() throws TechnicalException, IndicatorsException, CloneNotSupportedException {
         final Knowledge knowledge = Knowledge.load(TimeScale.DAILY);
         final String indicatorId = "mint";
         final IndicatorCategory expectedCat = IndicatorCategory.INDICATORS;
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/ColdsumtminTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/ColdsumtminTest.java
index c8c02d5a..49bbe200 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/ColdsumtminTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/ColdsumtminTest.java
@@ -20,8 +20,7 @@ package fr.inrae.agroclim.indicators.model.indicator;
 
 import static junit.framework.TestCase.assertEquals;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.criteria.RelationalOperator;
 import fr.inrae.agroclim.indicators.model.criteria.SimpleCriteria;
 import org.junit.Test;
@@ -53,7 +52,7 @@ public class ColdsumtminTest extends DataTestHelper {
     }
 
     @Test
-    public void test() throws TechnicalException, FunctionalException {
+    public void test() throws IndicatorsException {
         final Sum indicator = new Sum();
         final SimpleCriteria criteria = new SimpleCriteria();
         criteria.setVariable(Variable.TMIN);
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/DayOfYearTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/DayOfYearTest.java
index 7d769ac8..a9ef8c8c 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/DayOfYearTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/DayOfYearTest.java
@@ -21,8 +21,7 @@ import static org.junit.Assert.assertNull;
 
 import org.junit.Test;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.criteria.Criteria;
 import fr.inrae.agroclim.indicators.model.criteria.RelationalOperator;
 import fr.inrae.agroclim.indicators.model.criteria.SimpleCriteria;
@@ -76,7 +75,7 @@ public final class DayOfYearTest extends DataTestHelper {
         String error = null;
         try {
             result = indicator.computeSingleValue(climaticData);
-        } catch (TechnicalException | FunctionalException ex) {
+        } catch (final IndicatorsException ex) {
             error = ex.getClass().getCanonicalName() + " : " + ex.getMessage();
         }
         assertNull(error, error);
@@ -96,7 +95,7 @@ public final class DayOfYearTest extends DataTestHelper {
         String error = null;
         try {
             result = indicator.computeSingleValue(climaticData);
-        } catch (TechnicalException | FunctionalException ex) {
+        } catch (final IndicatorsException ex) {
             error = ex.getClass().getCanonicalName() + " : " + ex.getMessage();
         }
         assertNull(error, error);
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/DiffOfSumTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/DiffOfSumTest.java
index b371057d..92c0200a 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/DiffOfSumTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/DiffOfSumTest.java
@@ -5,7 +5,7 @@ import static org.junit.Assert.assertNull;
 
 import org.junit.Test;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.Knowledge;
 import fr.inrae.agroclim.indicators.model.criteria.SimpleCriteria;
@@ -62,7 +62,7 @@ public class DiffOfSumTest extends DataTestHelper {
         String error = null;
         try {
             result = indicator.computeSingleValue(climaticData);
-        } catch (TechnicalException | FunctionalException ex) {
+        } catch (final IndicatorsException ex) {
             error = ex.getClass().getCanonicalName() + " : " + ex.getMessage();
         }
         assertNull(error, error);
@@ -85,7 +85,7 @@ public class DiffOfSumTest extends DataTestHelper {
         String error = null;
         try {
             result = sumwd.computeSingleValue(climaticData);
-        } catch (TechnicalException | FunctionalException ex) {
+        } catch (final IndicatorsException ex) {
             error = ex.getClass().getCanonicalName() + " : " + ex.getMessage();
         }
         assertNull(error, error);
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/FormulaTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/FormulaTest.java
index 5b81634e..d132cdf4 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/FormulaTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/FormulaTest.java
@@ -36,8 +36,7 @@ import com.fasterxml.jackson.databind.MappingIterator;
 import com.fasterxml.jackson.dataformat.csv.CsvMapper;
 import com.fasterxml.jackson.dataformat.csv.CsvSchema;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.data.DataTestHelper;
 import fr.inrae.agroclim.indicators.model.data.climate.ClimaticDailyData;
 import fr.inrae.agroclim.indicators.model.data.climate.ClimaticResource;
@@ -121,7 +120,7 @@ public class FormulaTest extends DataTestHelper {
     }
 
     @Test
-    public void computeSingleValue() throws TechnicalException, FunctionalException {
+    public void computeSingleValue() throws IndicatorsException {
         assertNotNull(this.data);
         assertNotNull(this.data.getRh());
         assertNotNull(this.data.getTh());
@@ -132,7 +131,7 @@ public class FormulaTest extends DataTestHelper {
     }
 
     @Test
-    public void computeWithParameters() throws TechnicalException, FunctionalException {
+    public void computeWithParameters() throws IndicatorsException {
         assertNotNull(this.data);
         assertNotNull(this.data.getTh());
         final double paramA = 10.;
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/FrequencyTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/FrequencyTest.java
index 1b31ee44..2a0ea149 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/FrequencyTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/FrequencyTest.java
@@ -18,8 +18,7 @@
  */
 package fr.inrae.agroclim.indicators.model.indicator;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.criteria.NoCriteria;
 import fr.inrae.agroclim.indicators.model.criteria.SimpleCriteria;
 import fr.inrae.agroclim.indicators.model.data.DataTestHelper;
@@ -67,7 +66,7 @@ public class FrequencyTest extends DataTestHelper {
         String error = null;
         try {
             result = indicator.computeSingleValue(climaticData);
-        } catch (TechnicalException | FunctionalException ex) {
+        } catch (final IndicatorsException ex) {
             error = ex.getClass().getCanonicalName() + " : " + ex.getMessage();
         }
         assertTrue(error, error == null);
@@ -76,7 +75,7 @@ public class FrequencyTest extends DataTestHelper {
         nbOfDays.setCriteria(new NoCriteria());
         try {
             result = indicator.computeSingleValue(climaticData);
-        } catch (TechnicalException | FunctionalException ex) {
+        } catch (final IndicatorsException ex) {
             error = ex.getClass().getCanonicalName() + " : " + ex.getMessage();
         }
         assertTrue(error, error == null);
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/IndicatorTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/IndicatorTest.java
index b70220dd..78db4252 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/IndicatorTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/IndicatorTest.java
@@ -27,7 +27,7 @@ import java.util.Set;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.EvaluationType;
 import fr.inrae.agroclim.indicators.model.Knowledge;
@@ -54,8 +54,7 @@ public class IndicatorTest extends DataTestHelper {
         private static final long serialVersionUID = 1L;
 
         @Override
-        public Double compute(final Resource<? extends DailyData> data)
-                throws TechnicalException, FunctionalException {
+        public Double compute(final Resource<? extends DailyData> data) throws IndicatorsException {
             throw new UnsupportedOperationException("Not supported yet.");
         }
 
@@ -107,7 +106,7 @@ public class IndicatorTest extends DataTestHelper {
 		@Override
 		public void removeParameter(final Parameter param) {
 			throw new UnsupportedOperationException("Not supported yet.");
-			
+
 		}
 
     }
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/MaxWaveLengthTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/MaxWaveLengthTest.java
index 88867844..12912701 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/MaxWaveLengthTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/MaxWaveLengthTest.java
@@ -5,8 +5,7 @@ import static org.junit.Assert.assertTrue;
 
 import org.junit.Test;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.criteria.Criteria;
 import fr.inrae.agroclim.indicators.model.criteria.RelationalOperator;
 import fr.inrae.agroclim.indicators.model.criteria.SimpleCriteria;
@@ -53,7 +52,7 @@ public class MaxWaveLengthTest extends DataTestHelper {
         String error = null;
         try {
             result = instance.computeSingleValue(climaticData);
-        } catch (TechnicalException | FunctionalException ex) {
+        } catch (final IndicatorsException ex) {
             error = ex.getClass().getCanonicalName() + " : " + ex.getMessage();
         }
         assertTrue(error, error == null);
@@ -77,7 +76,7 @@ public class MaxWaveLengthTest extends DataTestHelper {
         String error = null;
         try {
             result = instance.computeSingleValue(climaticData);
-        } catch (TechnicalException | FunctionalException ex) {
+        } catch (final IndicatorsException ex) {
             error = ex.getClass().getCanonicalName() + " : " + ex.getMessage();
         }
         assertTrue(error, error == null);
@@ -86,17 +85,11 @@ public class MaxWaveLengthTest extends DataTestHelper {
 
     /**
      * Without criteria: raise exception.
+     * @throws IndicatorsException expected exception while compute
      */
-    @Test(expected = RuntimeException.class)
-    public void withoutCriteria() {
+    @Test(expected = IndicatorsException.class)
+    public void withoutCriteria() throws IndicatorsException {
         final MaxWaveLength instance = new MaxWaveLength();
-
-        String error = null;
-        try {
-            instance.computeSingleValue(climaticData);
-        } catch (TechnicalException | FunctionalException ex) {
-            error = ex.getClass().getCanonicalName() + " : " + ex.getMessage();
-        }
-        assertTrue(error, error == null);
+        instance.computeSingleValue(climaticData);
     }
 }
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/NumberOfDaysTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/NumberOfDaysTest.java
index 9b4dba30..4ad1a5d1 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/NumberOfDaysTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/NumberOfDaysTest.java
@@ -21,8 +21,7 @@ import static org.junit.Assert.assertTrue;
 
 import org.junit.Test;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.criteria.ComparisonCriteria;
 import fr.inrae.agroclim.indicators.model.criteria.Criteria;
 import fr.inrae.agroclim.indicators.model.criteria.RelationalOperator;
@@ -73,7 +72,7 @@ public final class NumberOfDaysTest extends DataTestHelper {
         String error = null;
         try {
             result = instance.compute(climaticData);
-        } catch (TechnicalException | FunctionalException ex) {
+        } catch (final IndicatorsException ex) {
             error = ex.getClass().getCanonicalName() + " : " + ex.getMessage();
         }
         assertTrue(error, error == null);
@@ -93,7 +92,7 @@ public final class NumberOfDaysTest extends DataTestHelper {
         String error = null;
         try {
             result = instance.computeSingleValue(climaticData);
-        } catch (TechnicalException | FunctionalException ex) {
+        } catch (final IndicatorsException ex) {
             error = ex.getClass().getCanonicalName() + " : " + ex.getMessage();
         }
         assertTrue(error, error == null);
@@ -114,7 +113,7 @@ public final class NumberOfDaysTest extends DataTestHelper {
         String error = null;
         try {
             result = instance.computeSingleValue(climaticData);
-        } catch (TechnicalException | FunctionalException ex) {
+        } catch (final IndicatorsException ex) {
             error = ex.getClass().getCanonicalName() + " : " + ex.getMessage();
         }
         assertTrue(error, error == null);
@@ -133,7 +132,7 @@ public final class NumberOfDaysTest extends DataTestHelper {
         String error = null;
         try {
             result = instance.computeSingleValue(climaticData);
-        } catch (TechnicalException | FunctionalException ex) {
+        } catch (final IndicatorsException ex) {
             error = ex.getClass().getCanonicalName() + " : " + ex.getMessage();
         }
         assertTrue(error, error == null);
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/NumberOfWavesTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/NumberOfWavesTest.java
index 368d348e..1e993f07 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/NumberOfWavesTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/NumberOfWavesTest.java
@@ -23,8 +23,7 @@ import static org.junit.Assert.assertTrue;
 
 import org.junit.Test;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.criteria.Criteria;
 import fr.inrae.agroclim.indicators.model.criteria.RelationalOperator;
 import fr.inrae.agroclim.indicators.model.criteria.SimpleCriteria;
@@ -75,7 +74,7 @@ public final class NumberOfWavesTest extends DataTestHelper {
         String error = null;
         try {
             result = instance.computeSingleValue(climaticData);
-        } catch (TechnicalException | FunctionalException ex) {
+        } catch (final IndicatorsException ex) {
             error = ex.getClass().getCanonicalName() + " : " + ex.getMessage();
         }
         assertTrue(error, error == null);
@@ -84,17 +83,11 @@ public final class NumberOfWavesTest extends DataTestHelper {
 
     /**
      * Without criteria: raise exception.
+     * @throws IndicatorsException expected exception while compute
      */
-    @Test(expected = RuntimeException.class)
-    public void withoutCriteria() {
+    @Test(expected = IndicatorsException.class)
+    public void withoutCriteria() throws IndicatorsException {
         final NumberOfWaves instance = new NumberOfWaves();
-
-        String error = null;
-        try {
-            instance.computeSingleValue(climaticData);
-        } catch (TechnicalException | FunctionalException ex) {
-            error = ex.getClass().getCanonicalName() + " : " + ex.getMessage();
-        }
-        assertTrue(error, error == null);
+        instance.computeSingleValue(climaticData);
     }
 }
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/PhaseLengthTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/PhaseLengthTest.java
index 60c03cd3..72dad372 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/PhaseLengthTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/PhaseLengthTest.java
@@ -18,8 +18,7 @@
  */
 package fr.inrae.agroclim.indicators.model.indicator;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.data.DataTestHelper;
 import fr.inrae.agroclim.indicators.model.data.climate.ClimaticResource;
 import static junit.framework.TestCase.assertEquals;
@@ -62,7 +61,7 @@ public class PhaseLengthTest extends DataTestHelper {
         String error = null;
         try {
             result = indicator.computeSingleValue(climaticData);
-        } catch (TechnicalException | FunctionalException ex) {
+        } catch (IndicatorsException ex) {
             error = ex.getClass().getCanonicalName() + " : " + ex.getMessage();
         }
         assertTrue(error, error == null);
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/PhotothermalQuotientTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/PhotothermalQuotientTest.java
index 2a648659..a38ca0fb 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/PhotothermalQuotientTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/PhotothermalQuotientTest.java
@@ -23,8 +23,7 @@ import static org.junit.Assert.assertNotEquals;
 
 import org.junit.Test;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.criteria.RelationalOperator;
 import fr.inrae.agroclim.indicators.model.criteria.SimpleCriteria;
 import fr.inrae.agroclim.indicators.model.data.DataTestHelper;
@@ -77,7 +76,7 @@ public class PhotothermalQuotientTest extends DataTestHelper {
     }
 
     @Test
-    public void computeSingleValue() throws TechnicalException, FunctionalException {
+    public void computeSingleValue() throws IndicatorsException {
         final Double radiationSum = climaticData.getData().stream().map(ClimaticDailyData::getRadiation)
                 .reduce(0d, Double::sum);
         final Double tmeanSum = climaticData.getData().stream().map(ClimaticDailyData::getTmean)
@@ -89,7 +88,7 @@ public class PhotothermalQuotientTest extends DataTestHelper {
     }
 
     @Test
-    public void withNormalization() throws TechnicalException, FunctionalException {
+    public void withNormalization() throws IndicatorsException {
         indicator.getDividend().setNormalizationFunction(new Sigmoid());
         indicator.getDivisor().setNormalizationFunction(new Sigmoid());
         final double actual = indicator.computeSingleValue(climaticData);
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/QuotientTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/QuotientTest.java
index c7d1b503..e2b255cd 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/QuotientTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/QuotientTest.java
@@ -21,8 +21,7 @@ import static org.junit.Assert.assertTrue;
 
 import org.junit.Test;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.criteria.Criteria;
 import fr.inrae.agroclim.indicators.model.criteria.RelationalOperator;
 import fr.inrae.agroclim.indicators.model.criteria.SimpleCriteria;
@@ -77,7 +76,7 @@ public final class QuotientTest extends DataTestHelper {
         String error = null;
         try {
             result = indicator.computeSingleValue(climaticData);
-        } catch (TechnicalException | FunctionalException ex) {
+        } catch (IndicatorsException ex) {
             error = ex.getClass().getCanonicalName() + " : " + ex.getMessage();
         }
         assertTrue(error, error == null);
@@ -97,7 +96,7 @@ public final class QuotientTest extends DataTestHelper {
         String error = null;
         try {
             result = ind.computeSingleValue(climaticData);
-        } catch (TechnicalException | FunctionalException ex) {
+        } catch (IndicatorsException ex) {
             error = ex.getClass().getCanonicalName() + " : " + ex.getMessage();
         }
         double expected = 10d;
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/SumTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/SumTest.java
index 65465aa4..31e2e51e 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/SumTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/SumTest.java
@@ -5,8 +5,7 @@ import static org.junit.Assert.assertNull;
 
 import org.junit.Test;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.criteria.NoCriteria;
 import fr.inrae.agroclim.indicators.model.criteria.SimpleCriteria;
 import fr.inrae.agroclim.indicators.model.data.DataTestHelper;
@@ -53,7 +52,7 @@ public class SumTest extends DataTestHelper {
         String error = null;
         try {
             result = indicator.computeSingleValue(climaticData);
-        } catch (TechnicalException | FunctionalException ex) {
+        } catch (final IndicatorsException ex) {
             error = ex.getClass().getCanonicalName() + " : " + ex.getMessage();
         }
         assertNull(error, error);
@@ -79,7 +78,7 @@ public class SumTest extends DataTestHelper {
         String error = null;
         try {
             result = indicator.computeSingleValue(climaticData);
-        } catch (TechnicalException | FunctionalException ex) {
+        } catch (final IndicatorsException ex) {
             error = ex.getClass().getCanonicalName() + " : " + ex.getMessage();
         }
         assertNull(error, error);
-- 
GitLab


From b309dc4b3e85d704476e15f315fe0269955f8447 Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Thu, 21 Dec 2023 11:43:15 +0100
Subject: [PATCH 03/20] Continuer

---
 .gitlab-ci.yml                                |  12 +-
 .../indicators/exception/ErrorCategory.java   |   7 +-
 .../exception/IndicatorsErrorCategory.java    |  13 +-
 .../exception/type/CommonErrorType.java       |  46 +++++++
 .../exception/type/ComputationErrorType.java  | 112 ++++++++++++-----
 .../exception/type/ResourceErrorType.java     | 117 +++++++++---------
 .../exception/type/XmlErrorType.java          |  49 ++++++++
 .../indicators/model/EvaluationSettings.java  |  11 +-
 .../agroclim/indicators/model/Knowledge.java  |  21 ++--
 .../model/data/ResourceManager.java           |   2 +-
 .../model/indicator/AggregationIndicator.java |   6 +-
 .../agroclim/indicators/xml/XMLUtil.java      |  49 ++++----
 .../indicators/resources/messages.properties  |  70 +++++++----
 .../resources/messages_fr.properties          |  57 +++++----
 .../exception/ErrorMessageTest.java           |  12 +-
 .../indicators/exception/ErrorTypeTest.java   |   3 +-
 .../model/EvaluationSettingsTest.java         |   3 +-
 .../EvaluationWithoutAggregationTest.java     |   6 +-
 .../indicators/model/KnowledgeDailyTest.java  |   7 +-
 .../indicators/model/KnowledgeHourlyTest.java |   4 +-
 .../indicators/model/KnowledgeTest.java       |  18 +--
 .../indicators/model/RaidayMeantTest.java     |   3 +-
 .../model/StageDeltaEvaluationTest.java       |   3 +-
 .../model/criteria/FormulaCriteriaTest.java   |   3 +-
 .../model/criteria/SimpleCriteriaTest.java    |   7 +-
 .../indicators/model/data/DataTestHelper.java |   4 +-
 .../model/data/ResourceManagerTest.java       |   2 +-
 .../model/data/climate/ClimateTest.java       |   6 +-
 .../data/phenology/PhenologyLoaderTest.java   |   6 +-
 .../model/data/soil/SoilCalculatorTest.java   |   4 +-
 .../model/indicator/AverageTest.java          |   3 +-
 .../model/indicator/DiffOfSumTest.java        |   5 +-
 .../model/indicator/ImplementationsTest.java  |   4 +-
 .../model/indicator/IndicatorTest.java        |   7 +-
 .../model/indicator/TammFormulaTest.java      |   4 +-
 .../listener/CompositeIndicatorTest.java      |   4 +-
 .../listener/PropertyChangeListenerTest.java  |   4 +-
 .../agroclim/indicators/xml/XMLUtilTest.java  |  19 ++-
 38 files changed, 455 insertions(+), 258 deletions(-)
 create mode 100644 src/main/java/fr/inrae/agroclim/indicators/exception/type/CommonErrorType.java
 create mode 100644 src/main/java/fr/inrae/agroclim/indicators/exception/type/XmlErrorType.java

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index d4d6140b..36caf5f4 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -18,7 +18,7 @@ cache:
 
 build_job:
   stage: build
-  script: 
+  script:
     - echo "Maven compile started"
     - mvn compile test-compile
     - ls -lha /usr/bin/tokei
@@ -26,9 +26,15 @@ build_job:
 
 test_job:
   stage: test
-  script: 
+  script:
     - echo "Maven test started"
     - mvn test
+  artifacts:
+    when: always
+    reports:
+      junit:
+        - target/surefire-reports/TEST-*.xml
+        - target/failsafe-reports/TEST-*.xml
 
 checkstyle_job:
   stage: checkstyle
@@ -47,7 +53,7 @@ cpd_job:
 
 package_job:
   stage: package
-  script: 
+  script:
     - echo "Maven packaging started"
     - mvn package -DskipTests
 
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/ErrorCategory.java b/src/main/java/fr/inrae/agroclim/indicators/exception/ErrorCategory.java
index fa2ed983..201e2c1e 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/exception/ErrorCategory.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/ErrorCategory.java
@@ -17,7 +17,7 @@ public interface ErrorCategory {
      * @return category description for the category code in I18n
      */
     default String getCategory(final I18n i18n) {
-        return i18n.get("error.category." + getCode());
+        return i18n.get("error.category." + getName().toLowerCase());
     }
 
     /**
@@ -25,4 +25,9 @@ public interface ErrorCategory {
      */
     String getCode();
 
+    /**
+     * @return short name, formatted as enum name.
+     */
+    String getName();
+
 }
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorsErrorCategory.java b/src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorsErrorCategory.java
index 3292bb30..1e25aa3a 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorsErrorCategory.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorsErrorCategory.java
@@ -18,14 +18,18 @@ import lombok.RequiredArgsConstructor;
  */
 @RequiredArgsConstructor
 public enum IndicatorsErrorCategory implements ErrorCategory {
+    /**
+     * While loading XML.
+     */
+    XML("IND01"),
     /**
      * For {@link ResourceManager}.
      */
-    RESOURCES("IND01"),
+    RESOURCES("IND02"),
     /**
      * While {@link Indicator#compute()}.
      */
-    COMPUTATION("IND02");
+    COMPUTATION("IND03");
 
     /**
      * Category code: prefix for {@link ErrorType#getFullCode()}.
@@ -33,4 +37,9 @@ public enum IndicatorsErrorCategory implements ErrorCategory {
     @Getter
     private final String code;
 
+    @Override
+    public String getName() {
+        return name();
+    }
+
 }
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/type/CommonErrorType.java b/src/main/java/fr/inrae/agroclim/indicators/exception/type/CommonErrorType.java
new file mode 100644
index 00000000..67e89b0e
--- /dev/null
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/type/CommonErrorType.java
@@ -0,0 +1,46 @@
+package fr.inrae.agroclim.indicators.exception.type;
+
+import fr.inrae.agroclim.indicators.exception.ErrorType;
+import java.util.StringJoiner;
+
+/**
+ * Common methods to implements ErrorType.
+ *
+ * @author Olivier Maury
+ */
+public interface CommonErrorType extends ErrorType {
+    /**
+     * @return partial I18n key for messages.properties
+     */
+    private String getShortKey() {
+        return getName().toLowerCase().replace("_", ".");
+    }
+
+    CommonErrorType getParent();
+
+    String name();
+
+    /**
+     * @return Key for Resource/I18nResource.
+     */
+    @Override
+    default String getI18nKey() {
+        final String cat = getCategory().getName().toLowerCase();
+        final StringJoiner sj = new StringJoiner(".", "error.", "");
+        if (!getShortKey().startsWith(cat)) {
+            sj.add(cat);
+        }
+        if (getParent() != null) {
+            if (!getShortKey().startsWith(getParent().getShortKey())) {
+                sj.add(getParent().getShortKey());
+            }
+        }
+        sj.add(getShortKey());
+        return sj.toString();
+    }
+
+    @Override
+    default String getName() {
+        return name();
+    }
+}
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/type/ComputationErrorType.java b/src/main/java/fr/inrae/agroclim/indicators/exception/type/ComputationErrorType.java
index c940efa6..3c3a79d0 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/exception/type/ComputationErrorType.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/type/ComputationErrorType.java
@@ -1,7 +1,6 @@
 package fr.inrae.agroclim.indicators.exception.type;
 
 import fr.inrae.agroclim.indicators.exception.ErrorCategory;
-import fr.inrae.agroclim.indicators.exception.ErrorType;
 import fr.inrae.agroclim.indicators.exception.IndicatorsErrorCategory;
 import fr.inrae.agroclim.indicators.model.indicator.CompositeIndicator;
 import lombok.Getter;
@@ -9,73 +8,144 @@ import lombok.RequiredArgsConstructor;
 
 /**
  * Keys from messages.properties used to warn about errors in {@link Indicator#compute()}.
+ *
+ * @author Olivier Maury
  */
 @RequiredArgsConstructor
-public enum ComputationErrorType implements ErrorType {
+public enum ComputationErrorType implements CommonErrorType {
     /**
      * Definition of the indicator is not good.
+     *
+     * Parameter 1 : explaination.
      */
     WRONG_DEFINITION(null, "001"),
     /**
-     * When an indicator fails to compute.
+     * When input causes an exception.
+     *
+     * No parameter.
      */
-    COMPUTATION(null, "100"),
+    INPUT(null, "100"),
     /**
      * When an indicator in a {@link CompositeIndicator} fails to compute.
+     *
+     * Parameter 1 : indicator id.
      */
-    COMPOSITE_COMPUTATION(COMPUTATION, "101"),
+    COMPOSITE_COMPUTATION(INPUT, "101"),
     /**
      * Criteria should be NoCriteria or SimpleCriteria.
+     *
+     * Parameter 1 : indicator id.
      */
     CRITERIA_ISNT_NOCRITERIA_SIMPLECRITERIA(WRONG_DEFINITION, "002"),
     /**
      * Criteria should not be null.
+     *
+     * No parameter.
      */
     CRITERIA_NULL(WRONG_DEFINITION, "003"),
     /**
      * Daily data must not be null.
+     *
+     * No parameter.
      */
-    DATA_NULL(COMPUTATION, "113"),
+    DATA_NULL(INPUT, "113"),
     /**
      * Dividend indicator should never be null.
+     *
+     * No parameter.
      */
     QUOTIENT_DIVIDEND_NULL(WRONG_DEFINITION, "005"),
     /**
      * Computation of dividend failed.
+     *
+     * Parameter 1 : indicator id.
      */
-    QUOTIENT_DIVIDEND_EXCEPTION(COMPUTATION, "110"),
+    QUOTIENT_DIVIDEND_EXCEPTION(INPUT, "110"),
     /**
      * Computation of divisor failed.
+     *
+     * Parameter 1 : indicator id.
      */
-    QUOTIENT_DIVISOR_EXCEPTION(COMPUTATION, "111"),
+    QUOTIENT_DIVISOR_EXCEPTION(INPUT, "111"),
     /**
      * Cannot compute quotient as result of divisor is zero.
+     *
+     * Parameter 1 : indicator id.
      */
-    QUOTIENT_DIVISOR_ZERO(COMPUTATION, "112"),
+    QUOTIENT_DIVISOR_ZERO(INPUT, "112"),
     /**
      * Divisor indicator should never be null.
+     *
+     * No parameter.
      */
     QUOTIENT_DIVISOR_NULL(WRONG_DEFINITION, "006"),
+    /**
+     * Execution of formula failed.
+     *
+     * parameter 1 : expression
+     */
     FORMULA(null, "200"),
+    /**
+     * Agregation to apply must not be null.
+     *
+     * No parameter.
+     */
     FORMULA_AGGREGATION_NULL(FORMULA, "211"),
+    /**
+     * Formula expression must not be null.
+     *
+     * No parameter.
+     */
     FORMULA_EXPRESSION_NULL(FORMULA, "221"),
+    /**
+     * Formula expression must not be blank.
+     *
+     * No parameter.
+     */
     FORMULA_EXPRESSION_BLANK(FORMULA, "222"),
+    /**
+     * Parsing error due to missing parenthesis.
+     *
+     * parameter 1 : expression
+     */
     FORMULA_EXPRESSION_PARENTHESIS(FORMULA, "223"),
+    /**
+     * Parsing error.
+     *
+     * parameter 1 : expression
+     */
     FORMULA_EXPRESSION_PARSING(FORMULA, "224"),
+    /**
+     * Unknown function.
+     *
+     * parameter 1 : expression
+     * parameter 2 : function
+     */
     FORMULA_FUNCTION_UNKNOWN(FORMULA, "231"),
+    /**
+     * Unknown variable.
+     *
+     * parameter 1 : expression
+     * parameter 2 : variable
+     */
     FORMULA_VARIABLE_UNDEFINED(FORMULA, "241"),
     /**
      * Threshold must not be null.
+     *
+     * No parameter.
      */
     THRESHOLD_NULL(WRONG_DEFINITION, "004"),
     /**
-     * Variable must not be null.
+     * Variable name must not be null.
      */
     VARIABLE_NAME_NULL(WRONG_DEFINITION, "007"),
     /**
      * Value for the variable must not be null.
+     *
+     * parameter 1 : date
+     * parameter 2 : variable name
      */
-    VARIABLE_VALUE_NULL(COMPUTATION, "114");
+    VARIABLE_VALUE_NULL(INPUT, "114");
 
     /**
      * Parent refers to the resource part.
@@ -93,25 +163,5 @@ public enum ComputationErrorType implements ErrorType {
     public ErrorCategory getCategory() {
         return IndicatorsErrorCategory.COMPUTATION;
     }
-    /**
-     * @return partial I18n key for messages.properties
-     */
-    private String getShortKey() {
-        return name().toLowerCase().replace("_", ".");
-    }
-    /**
-     * @return Key for Resource/I18nResource.
-     */
-    @Override
-    public String getI18nKey() {
-        if (parent != null) {
-            return "error.computation." + parent.getShortKey() + "." + getShortKey();
-        }
-        return "error.computation." + getShortKey();
-    }
 
-    @Override
-    public String getName() {
-        return name();
-    }
 }
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/type/ResourceErrorType.java b/src/main/java/fr/inrae/agroclim/indicators/exception/type/ResourceErrorType.java
index 6e1f6074..09631ddb 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/exception/type/ResourceErrorType.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/type/ResourceErrorType.java
@@ -1,91 +1,124 @@
 package fr.inrae.agroclim.indicators.exception.type;
 
 import fr.inrae.agroclim.indicators.exception.ErrorCategory;
-import fr.inrae.agroclim.indicators.exception.ErrorType;
 import fr.inrae.agroclim.indicators.exception.IndicatorsErrorCategory;
 import lombok.Getter;
+import lombok.RequiredArgsConstructor;
 
 /**
  * Keys from messages.properties used to warn about errors in {@link ResourceManager}.
+ *
+ * @author Olivier Maury
  */
-public enum ResourceErrorType implements ErrorType {
+@RequiredArgsConstructor
+public enum ResourceErrorType implements CommonErrorType {
     /**
      * Climate, topic.
+     *
+     * No parameter.
      */
-    CLIMATE("100", null, "resource.climatic"),
+    CLIMATE("100", null),
     /**
      * No climate data.
+     *
+     * No parameter.
+     */
+    CLIMATE_EMPTY("110", CLIMATE),
+    /**
+     * No climate data for the phase.
+     *
+     * parameter 0 : start stage name parameter 1 : end stage name parameter 2 : start stage day parameter 3 : end stage
+     * day parameter 4 : year parameter 5 : first available day parameter 6 ! last available day
      */
-    CLIMATE_EMPTY("110", CLIMATE, "empty"),
-    CLIMATE_EMPTY_FOR_PHASE("111", CLIMATE_EMPTY, "for.phase"),
+    CLIMATE_EMPTY_FOR_PHASE("111", CLIMATE_EMPTY),
     /**
      * Not enough data.
+     *
+     * No parameter.
      */
-    CLIMATE_SIZE_WRONG("101", CLIMATE, "size.wrong"),
+    CLIMATE_SIZE_WRONG("101", CLIMATE),
     /**
      * Years of climate, topic.
+     *
+     * No parameter.
      */
-    CLIMATIC_YEARS("120", null, "resource.climatic.years"),
+    CLIMATE_YEARS("120", null),
     /**
      * No years of climate.
+     *
+     * No parameter.
      */
-    CLIMATE_YEARS_EMPTY("121", CLIMATIC_YEARS, "empty"),
+    CLIMATE_YEARS_EMPTY("121", CLIMATE_YEARS),
     /**
      * Not enough data.
+     *
+     * No parameter.
      */
-    CLIMATE_YEARS_MISSING("122", CLIMATIC_YEARS, "missing"),
+    CLIMATE_YEARS_MISSING("122", CLIMATE_YEARS),
     /**
      * Phenology, topic.
+     *
+     * No parameter.
      */
-    PHENO("200", null, "resource.pheno"),
+    PHENO("200", null),
     /**
      * No phenological data.
+     *
+     * No parameter.
      */
-    PHENO_EMPTY("201", PHENO, "empty"),
+    PHENO_EMPTY("201", PHENO),
     /**
      * Years of phenology, topic.
+     *
+     * No parameter.
      */
-    PHENO_YEARS("210", null, "resource.pheno.years"),
+    PHENO_YEARS("210", PHENO),
     /**
      * No years of phenology.
+     *
+     * No parameter.
      */
-    PHENO_YEARS_EMPTY("211", PHENO_YEARS, "empty"),
-    /**
-     * Not enough data.
-     */
-    PHENO_YEARS_MISSING("212", PHENO_YEARS, "missing"),
+    PHENO_YEARS_EMPTY("211", PHENO_YEARS),
     /**
      * Resource in general, topic.
      */
-    RESOURCE("001", null, "resource"),
+    RESOURCES("001", null),
     /**
      * Setting not set.
+     *
+     * No parameter.
      */
-    RESOURCE_CROPDEVELOPMENT_YEARS("002", RESOURCE, "cropdevelopmentyears.null"),
+    RESOURCES_CROPDEVELOPMENT_YEARS("002", RESOURCES),
     /**
      * Soil, topic.
+     *
+     * No parameter.
      */
-    SOIL("300", null, "resource.soil"),
+    SOIL("300", null),
     /**
      * Not enough data.
+     *
+     * No parameter.
      */
-    SOIL_SIZE_WRONG("301", SOIL, "size.wrong"),
+    SOIL_SIZE_WRONG("301", SOIL),
     /**
      * Variables, topic.
+     *
+     * No parameter.
      */
-    VARIABLES("400", null, "resource.variables"),
+    VARIABLES("400", null),
     /**
      * No variable.
+     *
+     * No parameter.
      */
-    VARIABLES_EMPTY("401", VARIABLES, "empty"),
+    VARIABLES_EMPTY("401", VARIABLES),
     /**
      * No variale.
+     *
+     * No parameter.
      */
-    VARIABLES_MISSING("402", VARIABLES, "missing");
-    /**
-     * Key for Resource/I18nResource.
-     */
-    private final String key;
+    VARIABLES_MISSING("402", VARIABLES);
     /**
      * Subcode for the error.
      */
@@ -97,37 +130,9 @@ public enum ResourceErrorType implements ErrorType {
     @Getter
     private final ResourceErrorType parent;
 
-    /**
-     * Constructor.
-     *
-     * @param c Subcode for the error.
-     * @param p Parent refers to the resource part.
-     * @param k Key for Resource/I18nResource.
-     */
-    ResourceErrorType(final String c, final ResourceErrorType p, final String k) {
-        parent = p;
-        key = k;
-        subCode = c;
-    }
-
     @Override
     public ErrorCategory getCategory() {
         return IndicatorsErrorCategory.RESOURCES;
     }
 
-    /**
-     * @return Key for Resource/I18nResource.
-     */
-    @Override
-    public String getI18nKey() {
-        if (parent != null) {
-            return "error.evaluation." + parent.key + "." + key;
-        }
-        return "error.evaluation." + key;
-    }
-
-    @Override
-    public String getName() {
-        return name();
-    }
 }
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/type/XmlErrorType.java b/src/main/java/fr/inrae/agroclim/indicators/exception/type/XmlErrorType.java
new file mode 100644
index 00000000..aba636aa
--- /dev/null
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/type/XmlErrorType.java
@@ -0,0 +1,49 @@
+package fr.inrae.agroclim.indicators.exception.type;
+
+import fr.inrae.agroclim.indicators.exception.ErrorCategory;
+import fr.inrae.agroclim.indicators.exception.IndicatorsErrorCategory;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * Keys from messages.properties used to warn about errors in {@link Indicator#compute()}.
+ *
+ * @author Olivier Maury
+ */
+@RequiredArgsConstructor
+public enum XmlErrorType implements CommonErrorType {
+    /**
+     * XML file not found.
+     *
+     * Parameter 0 : file path
+     */
+    FILE_NOT_FOUND(null, "001"),
+    /**
+     * Unable to load.
+     *
+     * No parameter.
+     */
+    UNABLE_TO_LOAD(null, "002"),
+    /**
+     * Unable to serialize.
+     *
+     * No parameter.
+     */
+    UNABLE_TO_SERIALIZE(null, "003");
+    /**
+     * Parent refers to the resource part.
+     */
+    @Getter
+    private final XmlErrorType parent;
+
+    /**
+     * Subcode for the error.
+     */
+    @Getter
+    private final String subCode;
+
+    @Override
+    public ErrorCategory getCategory() {
+        return IndicatorsErrorCategory.XML;
+    }
+}
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/EvaluationSettings.java b/src/main/java/fr/inrae/agroclim/indicators/model/EvaluationSettings.java
index 47934cd8..1c26ae7c 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/EvaluationSettings.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/EvaluationSettings.java
@@ -16,6 +16,7 @@
  */
 package fr.inrae.agroclim.indicators.model;
 
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import java.io.Serializable;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -35,7 +36,6 @@ import javax.xml.bind.annotation.XmlTransient;
 import javax.xml.bind.annotation.XmlType;
 import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
 
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.criteria.ComparisonCriteria;
 import fr.inrae.agroclim.indicators.model.criteria.CompositeCriteria;
 import fr.inrae.agroclim.indicators.model.criteria.FormulaCriteria;
@@ -289,13 +289,10 @@ public final class EvaluationSettings implements Cloneable, Serializable {
 
     /**
      * Load Knowledge.
+     * @throws IndicatorsException while loading Knowledge
      */
-    public void initializeKnowledge() {
-        try {
-            knowledge = Knowledge.load(timescale);
-        } catch (final TechnicalException e) {
-            LOGGER.error("Loading Knowledge failed! {}", e);
-        }
+    public void initializeKnowledge() throws IndicatorsException {
+        knowledge = Knowledge.load(timescale);
     }
 
     /**
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/Knowledge.java b/src/main/java/fr/inrae/agroclim/indicators/model/Knowledge.java
index 44ed7a05..741c8993 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/Knowledge.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/Knowledge.java
@@ -16,6 +16,7 @@
  */
 package fr.inrae.agroclim.indicators.model;
 
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import java.io.BufferedWriter;
 import java.io.IOException;
 import java.io.InputStream;
@@ -41,7 +42,6 @@ import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlElementWrapper;
 import javax.xml.bind.annotation.XmlRootElement;
 
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.criteria.ComparisonCriteria;
 import fr.inrae.agroclim.indicators.model.criteria.CompositeCriteria;
 import fr.inrae.agroclim.indicators.model.criteria.FormulaCriteria;
@@ -152,9 +152,9 @@ public final class Knowledge implements Cloneable {
      * Deserialize Knowledge from embedded file for daily indicators.
      *
      * @return deserialized Knowledge
-     * @throws TechnicalException error while loading knowledge
+     * @throws IndicatorsException error while loading knowledge
      */
-    public static Knowledge load() throws TechnicalException {
+    public static Knowledge load() throws IndicatorsException {
         return load(TimeScale.DAILY);
     }
 
@@ -164,11 +164,10 @@ public final class Knowledge implements Cloneable {
      * @param stream
      *            input stream
      * @return deserialized Knowledge
-     * @throws TechnicalException
+     * @throws IndicatorsException
      *             error while loading knowledge
      */
-    private static Knowledge load(final InputStream stream)
-            throws TechnicalException {
+    private static Knowledge load(final InputStream stream) throws IndicatorsException {
         try {
             final Knowledge knowledge = (Knowledge) XMLUtil.loadResource(stream,
                     CLASSES_FOR_JAXB);
@@ -179,7 +178,7 @@ public final class Knowledge implements Cloneable {
             knowledge.culturalPractices.forEach(ind ->
             ind.setIndicatorCategory(IndicatorCategory.CULTURAL_PRATICES));
             return knowledge;
-        } catch (final TechnicalException ex) {
+        } catch (final IndicatorsException ex) {
             LOGGER.error(ex);
             throw ex;
         }
@@ -190,19 +189,19 @@ public final class Knowledge implements Cloneable {
      *
      * @param timescale timescale of indicators
      * @return deserialized Knowledge
-     * @throws TechnicalException error while loading knowledge
+     * @throws IndicatorsException error while loading knowledge
      */
-    public static Knowledge load(final TimeScale timescale) throws TechnicalException {
+    public static Knowledge load(final TimeScale timescale) throws IndicatorsException {
         final InputStream stream = Knowledge.class.getResourceAsStream(RESOURCES.get(timescale));
         return load(stream);
     }
 
     /**
      * @param args not used
-     * @throws TechnicalException while loading knowledge
+     * @throws IndicatorsException while loading knowledge
      * @throws java.io.IOException while writing file
      */
-    public static void main(final String[] args) throws TechnicalException,
+    public static void main(final String[] args) throws IndicatorsException,
     IOException {
         for (final TimeScale timescale: TimeScale.values()) {
             LOGGER.trace("Generating files for {}...", timescale);
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/data/ResourceManager.java b/src/main/java/fr/inrae/agroclim/indicators/model/data/ResourceManager.java
index 6c091cd1..31b74dd2 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/data/ResourceManager.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/data/ResourceManager.java
@@ -177,7 +177,7 @@ public final class ResourceManager implements Serializable, Cloneable {
             return errors;
         }
         if (cropDevelopmentYears == null) {
-            addErrorMessage(errors, ResourceErrorType.RESOURCE_CROPDEVELOPMENT_YEARS);
+            addErrorMessage(errors, ResourceErrorType.RESOURCES_CROPDEVELOPMENT_YEARS);
             return errors;
         }
         // Phenology data drives evaluation, so
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/AggregationIndicator.java b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/AggregationIndicator.java
index c64f4b32..142912ab 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/AggregationIndicator.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/AggregationIndicator.java
@@ -91,7 +91,7 @@ public abstract class AggregationIndicator extends SimpleIndicatorWithCriteria i
             return aggregate(stream);
         } catch (final RuntimeException ex) {
             if (ex.getCause() instanceof IndicatorsException iex) {
-                throw new IndicatorsException(ComputationErrorType.COMPUTATION, iex);
+                throw iex;
             }
             throw ex;
         }
@@ -100,7 +100,9 @@ public abstract class AggregationIndicator extends SimpleIndicatorWithCriteria i
     @Override
     public final Criteria getCriteria() {
         if (super.getCriteria() == null) {
-            super.setCriteria(new NoCriteria());
+            final var criteria = new NoCriteria();
+            criteria.setVariable(variable);
+            super.setCriteria(criteria);
         }
         if (super.getCriteria() instanceof VariableCriteria && this.variable != null) {
             ((VariableCriteria) super.getCriteria()).setVariable(this.variable);
diff --git a/src/main/java/fr/inrae/agroclim/indicators/xml/XMLUtil.java b/src/main/java/fr/inrae/agroclim/indicators/xml/XMLUtil.java
index c2a52340..3ad157fc 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/xml/XMLUtil.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/xml/XMLUtil.java
@@ -16,6 +16,7 @@
  */
 package fr.inrae.agroclim.indicators.xml;
 
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -38,12 +39,11 @@ import org.w3c.dom.ls.LSInput;
 import org.w3c.dom.ls.LSResourceResolver;
 import org.xml.sax.SAXException;
 
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.type.XmlErrorType;
 import fr.inrae.agroclim.indicators.model.EvaluationSettings;
 import fr.inrae.agroclim.indicators.model.Knowledge;
 import java.io.PrintWriter;
 import java.io.StringWriter;
-import java.util.HashMap;
 import java.util.Map;
 import lombok.Getter;
 import lombok.Setter;
@@ -220,10 +220,9 @@ public abstract class XMLUtil {
      * @return  Public ID of doctype → Resource for the doctype file.
      */
     public static Map<String, InputStream> getDtds() {
-        final Map<String, InputStream> map = new HashMap<>();
-        map.put(DTD_PUBLIC_ID_EVALUATION, getResourceAsStream("evaluation.dtd"));
-        map.put(DTD_PUBLIC_ID_KNOWLEDGE, getResourceAsStream("knowledge.dtd"));
-        return map;
+        return Map.of(//
+                DTD_PUBLIC_ID_EVALUATION, getResourceAsStream("evaluation.dtd"), //
+                DTD_PUBLIC_ID_KNOWLEDGE, getResourceAsStream("knowledge.dtd"));
     }
 
     /**
@@ -244,17 +243,16 @@ public abstract class XMLUtil {
      * @param clazz
      *            used classes
      * @return object for the XML file
-     * @throws TechnicalException
+     * @throws IndicatorsException
      *             exception from JAXBException
      */
-    public static Object load(final File xmlFile, final Class<?>... clazz)
-            throws TechnicalException {
+    public static Object load(final File xmlFile, final Class<?>... clazz) throws IndicatorsException {
         try (InputStream inputStream = new FileInputStream(xmlFile);) {
             return loadResource(inputStream, clazz);
         } catch (final FileNotFoundException ex) {
-            throw new TechnicalException("File not found " + xmlFile.getAbsolutePath(), ex);
+            throw new IndicatorsException(XmlErrorType.FILE_NOT_FOUND, ex, xmlFile.getAbsolutePath());
         } catch (final IOException ex) {
-            throw new TechnicalException("Unable to load ", ex);
+            throw new IndicatorsException(XmlErrorType.UNABLE_TO_LOAD, ex);
         }
     }
 
@@ -266,13 +264,12 @@ public abstract class XMLUtil {
      * @param clazz
      *            used classes
      * @return object for the XML file
-     * @throws TechnicalException
+     * @throws IndicatorsException
      *             exception from JAXBException
      */
-    public static Object loadResource(final InputStream inputStream, final Class<?>... clazz)
-            throws TechnicalException {
+    public static Object loadResource(final InputStream inputStream, final Class<?>... clazz) throws IndicatorsException {
         if (inputStream == null) {
-            throw new TechnicalException("InputStream should not be null!");
+            throw new IndicatorsException(XmlErrorType.FILE_NOT_FOUND, "InputStream should not be null!");
         }
         try {
             UnmarshallerBuilder builder = new UnmarshallerBuilder();
@@ -287,7 +284,7 @@ public abstract class XMLUtil {
             final PrintWriter pw = new PrintWriter(buffer);
             ex.printStackTrace(pw);
             LOGGER.fatal("Unable to deserialize : {}", buffer);
-            throw new TechnicalException("Unable to load", ex);
+            throw new IndicatorsException(XmlErrorType.UNABLE_TO_LOAD, ex);
         }
     }
 
@@ -301,15 +298,16 @@ public abstract class XMLUtil {
      *            output stream
      * @param clazz
      *            used classes
-     * @throws TechnicalException
+     * @throws IndicatorsException
      *             exception from JAXBException
      */
     public static void serialize(final Object o, final OutputStream outputStream, final Class<?>... clazz)
-                    throws TechnicalException {
+                    throws IndicatorsException {
         try (OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);) {
             serialize(o, writer, clazz);
         } catch (final IOException ex) {
-            throw new TechnicalException("Unable to create OutputStreamWriter : " + o, ex);
+            throw new IndicatorsException(XmlErrorType.UNABLE_TO_SERIALIZE, ex,
+                    "Unable to create OutputStreamWriter : " + o);
         }
     }
 
@@ -322,16 +320,17 @@ public abstract class XMLUtil {
      *            file path
      * @param clazz
      *            used classes
-     * @throws TechnicalException
+     * @throws IndicatorsException
      *             exception from JAXBException
      */
     public static void serialize(final Object o, final String fileName, final Class<?>... clazz)
-            throws TechnicalException {
+            throws IndicatorsException {
         try (FileOutputStream stream = new FileOutputStream(fileName);) {
             serialize(o, new OutputStreamWriter(stream, StandardCharsets.UTF_8), clazz);
         } catch (final IOException ex) {
             LOGGER.catching(ex);
-            throw new TechnicalException("Unable to create FileOutputStream : " + o, ex);
+            throw new IndicatorsException(XmlErrorType.UNABLE_TO_SERIALIZE, ex,
+                    "Unable to create FileOutputStream : " + o);
         }
     }
 
@@ -344,11 +343,11 @@ public abstract class XMLUtil {
      *            writer
      * @param clazz
      *            used classes
-     * @throws TechnicalException
+     * @throws IndicatorsException
      *             exception from JAXBException
      */
     private static void serialize(final Object o, final Writer writer, final Class<?>... clazz)
-            throws TechnicalException {
+            throws IndicatorsException {
         try {
             final MarshallerBuilder builder = new MarshallerBuilder();
             if (o instanceof EvaluationSettings) {
@@ -365,7 +364,7 @@ public abstract class XMLUtil {
             final PrintWriter pw = new PrintWriter(buffer);
             ex.printStackTrace(pw);
             LOGGER.fatal("Unable to serialize : " + buffer.toString(), ex);
-            throw new TechnicalException("Unable to serialize : " + o, ex);
+            throw new IndicatorsException(XmlErrorType.UNABLE_TO_SERIALIZE, ex);
         }
     }
 
diff --git a/src/main/resources/fr/inrae/agroclim/indicators/resources/messages.properties b/src/main/resources/fr/inrae/agroclim/indicators/resources/messages.properties
index aed9f283..dc7ffc32 100644
--- a/src/main/resources/fr/inrae/agroclim/indicators/resources/messages.properties
+++ b/src/main/resources/fr/inrae/agroclim/indicators/resources/messages.properties
@@ -17,31 +17,57 @@
 #Errors
 error.climate.jdbc.query=Error while query execution "{0}"!
 error.climate.dates=For the year {0}, available dates are from {1} to {2}.
-error.climate.missing=No climatic daily data for phase {0}-{1} (days {2} -> {3}) in {4}.
 error.climate.no.data=No data were retrieved.
 error.climate.wrong.headers=Wrong number of headers! {0} headers are in the file: {1}, but {2} headers are defined: {3}.
 error.computation.wrong.definition=The indicator "{0}" is not well defined: {1}.
 error.computation.wrong.definition.criteria.isnt.nocriteria.simplecriteria=criteria is neither NoCriteria nor SimpleCriteria!
 error.evaluation.resource=Error for resources!
 error.evaluation.resource.climatic=Error for climatic data set in resources!
-error.evaluation.resource.climatic.empty=No climatic data set in resources!
-error.evaluation.resource.climatic.size.wrong=The number of climatic data does not match whole years!
-error.evaluation.resource.climatic.years=Error for years in climatic data set in resources!
-error.evaluation.resource.climatic.years.empty=No year in climatic data set in resources!
-error.evaluation.resource.climatic.years.missing=Climatic data set in resources miss for years {0}!
-error.evaluation.resource.cropdevelopmentyears.null=The property ResourceManager.cropDevelopmentYears must not be null!
-error.evaluation.resource.pheno=Error for phenological data set in resources!
-error.evaluation.resource.pheno.empty=No phenological data set in resources!
-error.evaluation.resource.pheno.years=Error for years in phenological data set in resources!
-error.evaluation.resource.pheno.years.empty=No year in phenological data set in resources!
-error.evaluation.resource.pheno.years.missing=Phenological data set in resources miss for years {0}!
-error.evaluation.resource.soil=Error for soil data!
-error.evaluation.resource.soil.size.wrong=The number of soil data does not match whole years!
-error.evaluation.resource.variables=Error for the property ResourceManager.variables!
-error.evaluation.resource.variables.empty=The property ResourceManager.variables must not be empty!
-error.evaluation.resource.variables.missing=The property ResourceManager.variables must not be null!
+error.computation.formula=Error while executing expression "{0}".
+error.computation.formula.aggregation.null=Aggregation must not be null.
+error.computation.formula.expression.blank=Expression must not be empty.
+error.computation.formula.expression.null=Expression must not be null.
+error.computation.formula.expression.parenthesis=Invalid expression "{0}": missing parenthesis.
+error.computation.formula.expression.parsing=Invalid expression "{0}": parsing error.
+error.computation.formula.function.unknown=Invalid expression "{0}": the function "{1}" is unknown or ambiguous. If the function exists check arguments.
+error.computation.formula.variable.undefined=Invalid expression "{0}": the variable "{1}" is not defined.
+error.computation.input=
+error.computation.input.composite.computation=
+error.computation.input.data.null=
+error.computation.input.quotient.dividend.exception=
+error.computation.input.quotient.divisor.exception=
+error.computation.input.quotient.divisor.zero=
+error.computation.input.variable.value.null=
+error.computation.wrong.definition.criteria.null=
+error.computation.wrong.definition.quotient.dividend.null=
+error.computation.wrong.definition.quotient.divisor.null=
+error.computation.wrong.definition.threshold.null=
+error.computation.wrong.definition.variable.name.null=
 error.day.duplicate={0}: line {1}: this is the same date as previous line "{2}".
 error.day.missing={0}: line {1}: a day is missing before "{2}".
+error.resources.climate=
+error.resources=
+error.resources.cropdevelopment.years=
+error.resources.soil=Error for soil data!
+error.resources.soil.size.wrong=The number of soil data does not match whole years!
+error.resources.variables=Error for the property ResourceManager.variables!
+error.resources.variables.empty=The property ResourceManager.variables must not be empty!
+error.resources.variables.missing=The property ResourceManager.variables must not be null!
+error.resources.climate.empty.for.phase=No climatic daily data for phase {0}-{1} (from {2} to {3}) in {4}. Available data between {5} and {6}.
+error.resources.climate.empty=No climatic data set in resources!
+error.resources.climate.size.wrong=The number of climatic data does not match whole years!
+error.resources.climate.years.empty=No year in climatic data set in resources!
+error.resources.climate.years=Error for years in climatic data set in resources!
+error.resources.climate.years.missing=Climatic data set in resources miss for years {0}!
+error.resources.cropdevelopmentyears.null=The property ResourceManager.cropDevelopmentYears must not be null!
+error.resources.pheno.empty=No phenological data set in resources!
+error.resources.pheno=Error for phenological data set in resources!
+error.resources.pheno.years.empty=No year in phenological data set in resources!
+error.resources.pheno.years=Error for years in phenological data set in resources!
+error.resources.pheno.years.missing=Phenological data set in resources miss for years {0}!
+error.xml.file.not.found=
+error.xml.unable.to.load=
+error.xml.unable.to.serialize=
 error.day.null={0}: line {1}: day is required.
 error.day.succession={0}: line {1}: the day "{2}" is ealier than the day of the previous line "{3}".
 error.date.notread=The start and/or end date could not be read
@@ -95,14 +121,6 @@ normalization.Sigmoid=Sigmoid
 EvaluationType.WITH_AGGREGATION.name=with aggregation
 EvaluationType.WITHOUT_AGGREGATION.name=without aggregation
 
-JEXLFormula.error.execution=Error while executing expression "{0}".
-JEXLFormula.error.expression.null=Expression must not be null.
-JEXLFormula.error.expression.empty=Expression must not be empty.
-JEXLFormula.error.expression.parenthesis=Invalid expression "{0}": missing parenthesis.
-JEXLFormula.error.expression.parsing=Invalid expression "{0}": parsing error.
-JEXLFormula.error.variable.undefined=Invalid expression "{0}": the variable "{1}" is not defined.
-JEXLFormula.error.function.unknown=Invalid expression "{0}": the function "{1}" is unknown or ambiguous. If the function exists check arguments.
-
 MathMethod.avg.description = Returns the average of values passed as function parameter.
 MathMethod.exp.description = Returns Euler's number e raised to the power of a double value. Special cases:\n\n\
 If the argument is NaN, the result is NaN.\n\
@@ -142,4 +160,4 @@ Variable.RAIN.description=Rain precipitation [mm].
 Variable.RH.description=Relative humidity [%].
 Variable.SOILWATERCONTENT.description=Soil water content [% mass].
 Variable.WATER_RESERVE.description=Soil water reserve [mm].
-Variable.WIND.description=Wind speed [m/s].
\ No newline at end of file
+Variable.WIND.description=Wind speed [m/s].
diff --git a/src/main/resources/fr/inrae/agroclim/indicators/resources/messages_fr.properties b/src/main/resources/fr/inrae/agroclim/indicators/resources/messages_fr.properties
index 46bb1c77..090adcc7 100644
--- a/src/main/resources/fr/inrae/agroclim/indicators/resources/messages_fr.properties
+++ b/src/main/resources/fr/inrae/agroclim/indicators/resources/messages_fr.properties
@@ -21,27 +21,48 @@ warning.soilcalculator.4stages[one]=Un stade ph\u00e9nologique est fourni, mais
 warning.soilloader.missing=La configuration pour le calcul du bilan hydrique manque alors que des indicateurs portent sur la teneur en eau du sol ou la r\u00e9serve utile.
 warning.tmax.inferiorto.tmin={0}\u00a0: ligne {1}\u00a0: la temp\u00e9rature minimale doit \u00eatre inf\u00e9rieure \u00e0 la temp\u00e9rature maximale.
 error.climate.dates=Pour l\u2019ann\u00e9e {0}, les dates lues vont du {1} au {2}.
-error.climate.missing=Aucune donn\u00e9e climatique pour la phase {0}-{1} (jours {2} -> {3}) en {4}.
 error.climate.no.data=Aucune donn\u00e9e n\u2019a \u00e9t\u00e9 r\u00e9cup\u00e9r\u00e9e.
 error.climate.wrong.headers=Mauvais nombre d\u2019ent\u00eates\u00a0! {0} ent\u00eates sont dans le fichier : {1}, mais {2} ent\u00eates sont d\u00e9finis : {3}.
-error.evaluation.resource=Erreur pour les ressources\u00a0!
+error.computation.formula=Erreur lors de l\u2019ex\u00e9cution de l\u2019expression \u00ab {0} \u00bb.
+error.computation.formula.aggregation.null=L\u2019agr\u00e9gation ne peut \u00eatre nulle.
+error.computation.formula.expression.blank=L\u2019expression ne peut \u00eatre vide.
+error.computation.formula.expression.null=L\u2019expression ne peut \u00eatre nulle.
+error.computation.formula.expression.parenthesis=Expression non valide \u00ab {0} \u00bb : des parenth\u00e8ses manquent.
+error.computation.formula.expression.parsing=Expression non valide \u00ab {0} \u00bb : erreur d\u2019analyse.
+error.computation.formula.function.unknown=Expression non valide \u00ab {0} \u00bb : la fonction \u00ab {1} \u00bb est inconnue ou ambig\u00fce. Si la fonction existe, v\u00e9rifiez les arguments.
+error.computation.formula.variable.undefined=Expression non valide \u00ab {0} \u00bb : la variable \u00ab {1} \u00bb n\u2019est pas d\u00e9finie.
+
+error.computation.input=
+error.computation.input.composite.computation=
+error.computation.input.data.null=
+error.computation.input.quotient.dividend.exception=
+error.computation.input.quotient.divisor.exception=
+error.computation.input.quotient.divisor.zero=
+error.computation.input.variable.value.null=
+error.computation.wrong.definition.criteria.null=
+error.computation.wrong.definition.quotient.dividend.null=
+error.computation.wrong.definition.quotient.divisor.null=
+error.computation.wrong.definition.threshold.null=
+error.computation.wrong.definition.variable.name.null=
 error.evaluation.resource.climatic=Erreur pour les donn\u00e9es climatiques d\u00e9finies dans les ressources\u00a0!
-error.evaluation.resource.climatic.empty=Aucune donn\u00e9e climatique d\u00e9finie dans les ressources\u00a0!
-error.evaluation.resource.climatic.size.wrong=Le nombre de donn\u00e9es climatiques ne correspond pas \u00e0 des ann\u00e9es enti\u00e8res\u00a0!
-error.evaluation.resource.climatic.years.empty=Erreur pour les ann\u00e9es dans les donn\u00e9es climatiques d\u00e9finies des ressources\u00a0!
-error.evaluation.resource.climatic.years.empty=Aucune ann\u00e9e dans les donn\u00e9es climatiques d\u00e9finies des ressources\u00a0!
-error.evaluation.resource.climatic.years.missing=Les donn\u00e9es climatiques d\u00e9finies dans les ressources manquent pour les ann\u00e9es {0}\u00a0!
-error.evaluation.resource.cropdevelopmentyears.null=La propri\u00e9t\u00e9 ResourceManager.cropDevelopmentYears ne doit pas \u00eatre nulle\u00a0!
-error.evaluation.resource.pheno=Erreur pour les donn\u00e9es ph\u00e9nologiques d\u00e9finies dans les ressources\u00a0!
-error.evaluation.resource.pheno.empty=Aucune donn\u00e9e ph\u00e9nologique d\u00e9finie dans les ressources\u00a0!
-error.evaluation.resource.pheno.years=Erreur pour les ann\u00e9es dans les donn\u00e9es ph\u00e9nologiques des ressources\u00a0!
-error.evaluation.resource.pheno.years.empty=Aucune ann\u00e9e dans les donn\u00e9es ph\u00e9nologiques des ressources\u00a0!
-error.evaluation.resource.pheno.years.missing=Les donn\u00e9es ph\u00e9nologiques d\u00e9finies dans les ressources manquent pour les ann\u00e9es {0}\u00a0!
+error.evaluation.resource=Erreur pour les ressources\u00a0!
 error.evaluation.resource.soil=Erreur pour l donn\u00e9es de sol\u00a0!
 error.evaluation.resource.soil.size.wrong=Le nombre de donn\u00e9es de sol ne correspond pas \u00e0 des ann\u00e9es enti\u00e8res\u00a0!
-error.evaluation.resource.variables=Erreur pour la propri\u00e9t\u00e9 ResourceManager.variables\u00a0!
 error.evaluation.resource.variables.empty=La propri\u00e9t\u00e9 ResourceManager.variables ne doit pas \u00eatre vide\u00a0!
+error.evaluation.resource.variables=Erreur pour la propri\u00e9t\u00e9 ResourceManager.variables\u00a0!
 error.evaluation.resource.variables.missing=La propri\u00e9t\u00e9 ResourceManager.variables ne doit pas \u00eatre nulle\u00a0!
+error.resources.climate.empty=Aucune donn\u00e9e climatique d\u00e9finie dans les ressources\u00a0!
+error.resources.climate.empty.for.phase=Aucune donn\u00e9e climatique pour la phase {0}-{1} (de {2} \u00e0 {3}) en {4}. Donn\u00e9es disponibles entre {5} et {6}.
+error.resources.climate.size.wrong=Le nombre de donn\u00e9es climatiques ne correspond pas \u00e0 des ann\u00e9es enti\u00e8res\u00a0!
+error.resources.climate.years.empty=Aucune ann\u00e9e dans les donn\u00e9es climatiques d\u00e9finies des ressources\u00a0!
+error.resources.climate.years.empty=Erreur pour les ann\u00e9es dans les donn\u00e9es climatiques d\u00e9finies des ressources\u00a0!
+error.resources.climate.years.missing=Les donn\u00e9es climatiques d\u00e9finies dans les ressources manquent pour les ann\u00e9es {0}\u00a0!
+error.resources.cropdevelopmentyears.null=La propri\u00e9t\u00e9 ResourceManager.cropDevelopmentYears ne doit pas \u00eatre nulle\u00a0!
+error.resources.pheno.empty=Aucune donn\u00e9e ph\u00e9nologique d\u00e9finie dans les ressources\u00a0!
+error.resources.pheno=Erreur pour les donn\u00e9es ph\u00e9nologiques d\u00e9finies dans les ressources\u00a0!
+error.resources.pheno.years.empty=Aucune ann\u00e9e dans les donn\u00e9es ph\u00e9nologiques des ressources\u00a0!
+error.resources.pheno.years=Erreur pour les ann\u00e9es dans les donn\u00e9es ph\u00e9nologiques des ressources\u00a0!
+error.resources.pheno.years.missing=Les donn\u00e9es ph\u00e9nologiques d\u00e9finies dans les ressources manquent pour les ann\u00e9es {0}\u00a0!
 error.rh.outofrange={0}\u00a0: ligne {1}\u00a0: l\u2019humidit\u00e9 relative (%) doit \u00eatre comprise dans l\u2019intervalle 0-100.
 error.title=Erreur
 error.year.null={0}\u00a0: ligne {1}\u00a0: l\u2019ann\u00e9e est obligatoire.
@@ -88,14 +109,6 @@ normalization.MultiLinear=Affine par morceaux
 normalization.Normal=Normale
 normalization.Sigmoid=Sigmo\u00efde
 
-JEXLFormula.error.execution=Erreur lors de l\u2019ex\u00e9cution de l\u2019expression \u00ab {0} \u00bb.
-JEXLFormula.error.expression.null=L\u2019expression ne peut \u00eatre nulle.
-JEXLFormula.error.expression.empty=L\u2019expression ne peut \u00eatre vide.
-JEXLFormula.error.expression.parenthesis=Expression non valide \u00ab {0} \u00bb : des parenth\u00e8ses manquent.
-JEXLFormula.error.expression.parsing=Expression non valide \u00ab {0} \u00bb : erreur d\u2019analyse.
-JEXLFormula.error.variable.undefined=Expression non valide \u00ab {0} \u00bb : la variable \u00ab {1} \u00bb n\u2019est pas d\u00e9finie.
-JEXLFormula.error.function.unknown=Expression non valide \u00ab {0} \u00bb : la fonction \u00ab {1} \u00bb est inconnue ou ambig\u00fce. Si la fonction existe, v\u00e9rifiez les arguments.
-
 EvaluationType.WITH_AGGREGATION.name=avec agr\u00e9gation
 EvaluationType.WITHOUT_AGGREGATION.name=sans agr\u00e9gation
 
diff --git a/src/test/java/fr/inrae/agroclim/indicators/exception/ErrorMessageTest.java b/src/test/java/fr/inrae/agroclim/indicators/exception/ErrorMessageTest.java
index 91135245..9d8cbcaa 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/exception/ErrorMessageTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/exception/ErrorMessageTest.java
@@ -52,7 +52,17 @@ public class ErrorMessageTest {
 
         @Override
         public ErrorCategory getCategory() {
-            return () -> "TEST00";
+            return new ErrorCategory() {
+                @Override
+                public String getCode() {
+                    return "TEST00";
+                }
+
+                @Override
+                public String getName() {
+                    return "CATEGORY";
+                }
+            };
         }
 
         @Override
diff --git a/src/test/java/fr/inrae/agroclim/indicators/exception/ErrorTypeTest.java b/src/test/java/fr/inrae/agroclim/indicators/exception/ErrorTypeTest.java
index 00cda328..863d0896 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/exception/ErrorTypeTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/exception/ErrorTypeTest.java
@@ -2,6 +2,7 @@ package fr.inrae.agroclim.indicators.exception;
 
 import fr.inrae.agroclim.indicators.exception.type.ComputationErrorType;
 import fr.inrae.agroclim.indicators.exception.type.ResourceErrorType;
+import fr.inrae.agroclim.indicators.exception.type.XmlErrorType;
 import static org.junit.Assert.assertTrue;
 
 import java.util.ArrayList;
@@ -39,7 +40,7 @@ public class ErrorTypeTest {
      */
     @Parameterized.Parameters
     public static List<Class<? extends Enum<?>>> data() {
-        return List.of(ComputationErrorType.class, ResourceErrorType.class);
+        return List.of(ComputationErrorType.class, ResourceErrorType.class, XmlErrorType.class);
     }
 
     /**
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/EvaluationSettingsTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/EvaluationSettingsTest.java
index 06872d5d..d5849da6 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/EvaluationSettingsTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/EvaluationSettingsTest.java
@@ -16,6 +16,7 @@
  */
 package fr.inrae.agroclim.indicators.model;
 
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import static org.junit.Assert.assertTrue;
 import org.junit.Test;
 
@@ -30,7 +31,7 @@ import org.junit.Test;
 public final class EvaluationSettingsTest {
 
     @Test
-    public void initializeKnowledge() {
+    public void initializeKnowledge() throws IndicatorsException {
         EvaluationSettings settings = new EvaluationSettings();
         settings.initializeKnowledge();
         assertTrue("Knowledge should not be null!",
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/EvaluationWithoutAggregationTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/EvaluationWithoutAggregationTest.java
index e9a8794d..6bc18824 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/EvaluationWithoutAggregationTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/EvaluationWithoutAggregationTest.java
@@ -19,7 +19,6 @@
 package fr.inrae.agroclim.indicators.model;
 
 import fr.inrae.agroclim.indicators.exception.IndicatorsException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.data.DataTestHelper;
 import fr.inrae.agroclim.indicators.model.indicator.CompositeIndicator;
 import fr.inrae.agroclim.indicators.model.indicator.Indicator;
@@ -98,12 +97,11 @@ public class EvaluationWithoutAggregationTest extends DataTestHelper {
     }
     /**
      * Test save() a file.
-     * @throws fr.inrae.agroclim.indicators.exception.TechnicalException while
-     * serializing or loading
+     * @throws IndicatorsException while serializing or loading
      * @throws java.io.IOException while creating tmp file or writing
      */
     @Test(expected = Test.None.class)
-    public void save() throws TechnicalException, IOException {
+    public void save() throws IndicatorsException, IOException {
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
         XMLUtil.serialize(evaluation.getSettings(), baos,
                 EvaluationSettings.CLASSES_FOR_JAXB);
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/KnowledgeDailyTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/KnowledgeDailyTest.java
index 1a623d73..7ccdb768 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/KnowledgeDailyTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/KnowledgeDailyTest.java
@@ -16,10 +16,12 @@
  */
 package fr.inrae.agroclim.indicators.model;
 
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import java.util.Arrays;
 import java.util.HashMap;
@@ -32,7 +34,6 @@ import java.util.stream.Collectors;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.data.Variable;
 import fr.inrae.agroclim.indicators.model.indicator.CompositeIndicator;
 import fr.inrae.agroclim.indicators.model.indicator.Indicator;
@@ -57,8 +58,8 @@ public final class KnowledgeDailyTest {
     public static void loadXml() {
         try {
             knowledge = Knowledge.load(TimeScale.DAILY);
-        } catch (final TechnicalException ex) {
-            assertTrue("Loading XML file from Knowledge.load() should work!", false);
+        } catch (final IndicatorsException ex) {
+            fail("Loading XML file from Knowledge.load() should work!");
         }
     }
 
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/KnowledgeHourlyTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/KnowledgeHourlyTest.java
index 101f19be..e4e0935e 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/KnowledgeHourlyTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/KnowledgeHourlyTest.java
@@ -18,7 +18,7 @@
  */
 package fr.inrae.agroclim.indicators.model;
 
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.criteria.Criteria;
 import fr.inrae.agroclim.indicators.model.criteria.FormulaCriteria;
 import fr.inrae.agroclim.indicators.model.indicator.CompositeIndicator;
@@ -51,7 +51,7 @@ public class KnowledgeHourlyTest {
     public static void loadXml() {
         try {
             knowledge = Knowledge.load(TimeScale.HOURLY);
-        } catch (final TechnicalException ex) {
+        } catch (final IndicatorsException ex) {
             assertTrue("Loading XML file from Knowledge.load() should work!", false);
         }
     }
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/KnowledgeTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/KnowledgeTest.java
index 3b44ce15..651393b6 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/KnowledgeTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/KnowledgeTest.java
@@ -16,10 +16,12 @@
  */
 package fr.inrae.agroclim.indicators.model;
 
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -33,13 +35,13 @@ import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.data.Variable;
 import fr.inrae.agroclim.indicators.model.indicator.CompositeIndicator;
 import fr.inrae.agroclim.indicators.model.indicator.Indicator;
 import fr.inrae.agroclim.indicators.model.indicator.IndicatorCategory;
 import fr.inrae.agroclim.indicators.model.indicator.NumberOfWaves;
 import fr.inrae.agroclim.indicators.model.indicator.Quotient;
+import java.util.Objects;
 
 /**
  * Test the class vs the XML file.
@@ -71,9 +73,8 @@ public final class KnowledgeTest {
     public KnowledgeTest(final TimeScale timeScale) {
         try {
             knowledge = Knowledge.load(timeScale);
-        } catch (final TechnicalException ex) {
-            assertTrue("Loading XML file from Knowledge.load() should work!",
-                    false);
+        } catch (final IndicatorsException ex) {
+            fail("Loading XML file from Knowledge.load() should work!");
         }
     }
 
@@ -347,15 +348,14 @@ public final class KnowledgeTest {
     @Test
     public void noNullVariables() {
         getSimpleIndicators().forEach((ind) -> {
-            System.out.println("=> " + ind);
             assertNotNull(ind);
             final String id = ind.getId();
             final Set<Variable> vars = ind.getVariables();
             assertNotNull(id + ".variables must not be a null set.", vars);
-            assertTrue(id + ".variables must contain at least one variable.",
-                    !vars.isEmpty());
-            assertTrue(id + ".variables must not contains null.",
-                    !vars.contains(null));
+            assertTrue(id + ".variables must contain at least one variable.", !vars.isEmpty());
+            // with unmodifiable set, contains(null) throws NullPointerException
+            final boolean contains = vars.stream().anyMatch(Objects::isNull);
+            assertFalse(id + ".variables must not contains null. variables=" + vars, contains);
         });
 
     }
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/RaidayMeantTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/RaidayMeantTest.java
index ffacfcdc..1c71ef25 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/RaidayMeantTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/RaidayMeantTest.java
@@ -37,7 +37,6 @@ import org.junit.Before;
 import org.junit.Test;
 
 import fr.inrae.agroclim.indicators.exception.IndicatorsException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.criteria.RelationalOperator;
 import fr.inrae.agroclim.indicators.model.criteria.SimpleCriteria;
 import fr.inrae.agroclim.indicators.model.data.DataTestHelper;
@@ -180,7 +179,7 @@ public class RaidayMeantTest extends DataTestHelper {
             settings = (EvaluationSettings) XMLUtil.load(xmlFile,
                     EvaluationSettings.CLASSES_FOR_JAXB);
             settings.setFilePath(xmlFile.getAbsolutePath());
-        } catch (final TechnicalException ex) {
+        } catch (final IndicatorsException ex) {
             LOGGER.error(ex.getLocalizedMessage());
             return;
         }
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/StageDeltaEvaluationTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/StageDeltaEvaluationTest.java
index 38b9d376..8e5b6573 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/StageDeltaEvaluationTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/StageDeltaEvaluationTest.java
@@ -28,7 +28,6 @@ import org.junit.Before;
 import org.junit.Test;
 
 import fr.inrae.agroclim.indicators.exception.IndicatorsException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.data.DataTestHelper;
 import fr.inrae.agroclim.indicators.xml.XMLUtil;
 import lombok.extern.log4j.Log4j2;
@@ -56,7 +55,7 @@ public class StageDeltaEvaluationTest extends DataTestHelper {
                     EvaluationSettings.CLASSES_FOR_JAXB);
             settings.initializeKnowledge();
             settings.setFilePath(xmlFile.getAbsolutePath());
-        } catch (final TechnicalException ex) {
+        } catch (final IndicatorsException ex) {
             LOGGER.error(ex.getLocalizedMessage());
             return;
         }
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/criteria/FormulaCriteriaTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/criteria/FormulaCriteriaTest.java
index 63820714..deb63d0b 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/criteria/FormulaCriteriaTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/criteria/FormulaCriteriaTest.java
@@ -33,7 +33,6 @@ import java.util.Set;
 
 import org.junit.Test;
 
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.EvaluationSettings;
 import fr.inrae.agroclim.indicators.model.ExpressionParameter;
 import fr.inrae.agroclim.indicators.model.data.Variable;
@@ -81,7 +80,7 @@ public class FormulaCriteriaTest {
     }
 
     @Test
-    public void formulaWithParameter() throws IndicatorsException, TechnicalException {
+    public void formulaWithParameter() throws IndicatorsException {
         final FormulaCriteria crit = new FormulaCriteria();
         crit.setExpression("formulaCriteria:between(TMEAN, min, max)");
         final List<ExpressionParameter> expressionParameters = new ArrayList<>();
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/criteria/SimpleCriteriaTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/criteria/SimpleCriteriaTest.java
index a74af17e..774fefe1 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/criteria/SimpleCriteriaTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/criteria/SimpleCriteriaTest.java
@@ -14,7 +14,6 @@ import java.nio.charset.StandardCharsets;
 
 import org.junit.Test;
 
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.data.Variable;
 import fr.inrae.agroclim.indicators.model.data.climate.ClimaticDailyData;
 import fr.inrae.agroclim.indicators.xml.XMLUtil;
@@ -87,7 +86,7 @@ public class SimpleCriteriaTest {
     }
 
     @Test(expected = Test.None.class)
-    public void serializeInferiorToThreshold() throws TechnicalException, UnsupportedEncodingException {
+    public void serializeInferiorToThreshold() throws IndicatorsException, UnsupportedEncodingException {
         final Class<?>[] classes = new Class<?>[]{ Criteria.class, SimpleCriteria.class };
         final String expected = """
                                 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
@@ -112,7 +111,7 @@ public class SimpleCriteriaTest {
     }
 
     @Test(expected = Test.None.class)
-    public void unserializeGT() throws IndicatorsException, TechnicalException {
+    public void unserializeGT() throws IndicatorsException {
         final Class<?>[] classes = new Class<?>[]{ Criteria.class, SimpleCriteria.class };
         final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"
                 + "<criteria xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"simpleCriteria\">"
@@ -132,7 +131,7 @@ public class SimpleCriteriaTest {
     }
 
     @Test(expected = Test.None.class)
-    public void unserializeInferiorToThreshold() throws IndicatorsException, TechnicalException {
+    public void unserializeInferiorToThreshold() throws IndicatorsException {
         final Class<?>[] classes = new Class<?>[]{ Criteria.class, SimpleCriteria.class };
         final String xml = """
                            <?xml version="1.0" encoding="UTF-8" ?><criteria xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="simpleCriteria">    <variable>th</variable>
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/data/DataTestHelper.java b/src/test/java/fr/inrae/agroclim/indicators/model/data/DataTestHelper.java
index ea7c4864..f2394894 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/data/DataTestHelper.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/data/DataTestHelper.java
@@ -16,6 +16,7 @@
  */
 package fr.inrae.agroclim.indicators.model.data;
 
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -28,7 +29,6 @@ import java.util.Map;
 import java.util.Properties;
 import java.util.stream.Collectors;
 
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.Evaluation;
 import fr.inrae.agroclim.indicators.model.EvaluationSettings;
 import fr.inrae.agroclim.indicators.model.data.climate.ClimateFileLoader;
@@ -269,7 +269,7 @@ public abstract class DataTestHelper {
             settings = (EvaluationSettings) XMLUtil.load(file, EvaluationSettings.CLASSES_FOR_JAXB);
             settings.initializeKnowledge();
             settings.setFilePath(file.getAbsolutePath());
-        } catch (final TechnicalException ex) {
+        } catch (final IndicatorsException ex) {
             LOGGER.error(ex.getLocalizedMessage());
             return null;
         }
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/data/ResourceManagerTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/data/ResourceManagerTest.java
index d69f5cfe..fea30030 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/data/ResourceManagerTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/data/ResourceManagerTest.java
@@ -151,7 +151,7 @@ public final class ResourceManagerTest extends DataTestHelper {
         final Map<ResourceErrorType, ErrorMessage> errors = mgr.getConsitencyErrors();
         assertNotNull(errors);
         final Set<ResourceErrorType> expectedErrors = new HashSet<>();
-        expectedErrors.add(ResourceErrorType.CLIMATIC_YEARS);
+        expectedErrors.add(ResourceErrorType.CLIMATE_YEARS);
         final String subject = "ResourceManager with only climatic data in 2015 "
                 + "must have errors on climatic resources";
         assertEquals(subject, expectedErrors, errors.keySet());
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/data/climate/ClimateTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/data/climate/ClimateTest.java
index f9d1d7c9..f5b291a9 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/data/climate/ClimateTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/data/climate/ClimateTest.java
@@ -16,6 +16,7 @@
  */
 package fr.inrae.agroclim.indicators.model.data.climate;
 
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
@@ -26,7 +27,6 @@ import java.util.Map;
 
 import org.junit.Test;
 
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.EvaluationSettings;
 import fr.inrae.agroclim.indicators.model.data.DataTestHelper;
 
@@ -62,8 +62,8 @@ public final class ClimateTest extends DataTestHelper {
             assertNotNull("Loaded ClimaticDailyData list must not be null", data);
             assertFalse("Loaded ClimaticDailyData list must not be empty",
                     data.isEmpty());
-        } catch (final TechnicalException ex) {
-            error = ex.getMessage() + " " + ex.getRootException().getMessage();
+        } catch (final IndicatorsException ex) {
+            error = ex.getMessage() + " " + ex.getCause().getMessage();
         }
         assertNull(error, error);
     }
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/data/phenology/PhenologyLoaderTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/data/phenology/PhenologyLoaderTest.java
index 949a930a..ee655a14 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/data/phenology/PhenologyLoaderTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/data/phenology/PhenologyLoaderTest.java
@@ -16,6 +16,7 @@
  */
 package fr.inrae.agroclim.indicators.model.data.phenology;
 
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import static org.junit.Assert.assertTrue;
 
 import java.io.File;
@@ -23,7 +24,6 @@ import java.util.List;
 
 import org.junit.Test;
 
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.EvaluationSettings;
 import fr.inrae.agroclim.indicators.model.data.DataTestHelper;
 import fr.inrae.agroclim.indicators.xml.XMLUtil;
@@ -59,8 +59,8 @@ public final class PhenologyLoaderTest extends DataTestHelper {
             List<AnnualStageData> data = e.getPhenologyLoader().load();
             assertTrue("loaded data must not be null", data != null);
             assertTrue("loaded data must not be empty", !data.isEmpty());
-        } catch (final TechnicalException ex) {
-            error = ex.getMessage() + " " + ex.getRootException().getMessage();
+        } catch (final IndicatorsException ex) {
+            error = ex.getMessage() + " " + ex.getCause().getMessage();
         }
         assertTrue(error, error == null);
     }
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/data/soil/SoilCalculatorTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/data/soil/SoilCalculatorTest.java
index 5f87c24c..47e88c4a 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/data/soil/SoilCalculatorTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/data/soil/SoilCalculatorTest.java
@@ -17,7 +17,6 @@
 package fr.inrae.agroclim.indicators.model.data.soil;
 
 import fr.inrae.agroclim.indicators.exception.ErrorMessage;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.exception.type.ResourceErrorType;
 import fr.inrae.agroclim.indicators.model.Evaluation;
 import fr.inrae.agroclim.indicators.model.EvaluationSettings;
@@ -349,11 +348,10 @@ public final class SoilCalculatorTest extends DataTestHelper {
     /**
      * Test integration into {@link Evaluation}.
      *
-     * @throws TechnicalException if knowledge does not load.
      * @throws IOException while loading phenology properties
      */
     @Test
-    public void loadInEvaluation() throws TechnicalException, IOException {
+    public void loadInEvaluation() throws IOException {
         final String baseName = "pheno_curve_grapevine_sw_4_stages-chardonnay";
         final Properties ini = getPhenologyProperties(baseName);
         // 1. load Evaluation
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/AverageTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/AverageTest.java
index 3014fe06..7433ba01 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/AverageTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/AverageTest.java
@@ -33,7 +33,6 @@ import java.nio.file.Path;
 import org.junit.Test;
 
 import fr.inrae.agroclim.indicators.exception.IndicatorsException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.Knowledge;
 import fr.inrae.agroclim.indicators.model.TimeScale;
 import fr.inrae.agroclim.indicators.model.criteria.NoCriteria;
@@ -94,7 +93,7 @@ public class AverageTest extends DataTestHelper {
     }
 
     @Test
-    public void mint() throws TechnicalException, IndicatorsException, CloneNotSupportedException {
+    public void mint() throws IndicatorsException, CloneNotSupportedException {
         final Knowledge knowledge = Knowledge.load(TimeScale.DAILY);
         final String indicatorId = "mint";
         final IndicatorCategory expectedCat = IndicatorCategory.INDICATORS;
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/DiffOfSumTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/DiffOfSumTest.java
index 92c0200a..8291e26f 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/DiffOfSumTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/DiffOfSumTest.java
@@ -6,7 +6,6 @@ import static org.junit.Assert.assertNull;
 import org.junit.Test;
 
 import fr.inrae.agroclim.indicators.exception.IndicatorsException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.Knowledge;
 import fr.inrae.agroclim.indicators.model.criteria.SimpleCriteria;
 import fr.inrae.agroclim.indicators.model.data.DataTestHelper;
@@ -72,9 +71,9 @@ public class DiffOfSumTest extends DataTestHelper {
 
     /**
      * sumwd is also defined in knowledge.xml
-     * @throws TechnicalException while reading the XML
+     * @throws IndicatorsException while reading the XML
      */
-    public void sumwdFromKnowledge() throws TechnicalException {
+    public void sumwdFromKnowledge() throws IndicatorsException {
         final Knowledge knowledge = Knowledge.load();
         final SimpleIndicator sumwd = (SimpleIndicator) knowledge.getIndicator("sumwd");
         final double expected = climaticData.getData().stream()
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/ImplementationsTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/ImplementationsTest.java
index 69b9dca8..ebe244b2 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/ImplementationsTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/ImplementationsTest.java
@@ -16,7 +16,7 @@
  */
 package fr.inrae.agroclim.indicators.model.indicator;
 
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.Knowledge;
 import java.util.ArrayList;
 import java.util.List;
@@ -61,7 +61,7 @@ public class ImplementationsTest {
                     indicators.add(ind);
                 });
             }
-        } catch (final TechnicalException ex) {
+        } catch (final IndicatorsException ex) {
             LOGGER.fatal("Loading knowledge should not fail!", ex);
         }
         return indicators;
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/IndicatorTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/IndicatorTest.java
index 78db4252..0ca49112 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/IndicatorTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/IndicatorTest.java
@@ -19,7 +19,7 @@ package fr.inrae.agroclim.indicators.model.indicator;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import java.util.Map;
 import java.util.Set;
@@ -28,7 +28,6 @@ import org.junit.BeforeClass;
 import org.junit.Test;
 
 import fr.inrae.agroclim.indicators.exception.IndicatorsException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.EvaluationType;
 import fr.inrae.agroclim.indicators.model.Knowledge;
 import fr.inrae.agroclim.indicators.model.Parameter;
@@ -119,8 +118,8 @@ public class IndicatorTest extends DataTestHelper {
     public static void loadXml() {
         try {
             knowledge = Knowledge.load(TimeScale.DAILY);
-        } catch (final TechnicalException ex) {
-            assertTrue("Loading XML file from Knowledge.load() should work!", false);
+        } catch (final IndicatorsException ex) {
+            fail("Loading XML file from Knowledge.load() should work!");
         }
     }
     @Test
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/TammFormulaTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/TammFormulaTest.java
index afa28025..846182dd 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/TammFormulaTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/TammFormulaTest.java
@@ -33,8 +33,6 @@ import com.fasterxml.jackson.databind.MappingIterator;
 import com.fasterxml.jackson.dataformat.csv.CsvMapper;
 import com.fasterxml.jackson.dataformat.csv.CsvSchema;
 
-import fr.inrae.agroclim.indicators.exception.FunctionalException;
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.data.DataTestHelper;
 import fr.inrae.agroclim.indicators.model.data.climate.ClimaticDailyData;
 import lombok.Data;
@@ -123,7 +121,7 @@ public class TammFormulaTest extends DataTestHelper {
     }
 
     @Test
-    public void computeDailyValue() throws TechnicalException, FunctionalException {
+    public void computeDailyValue() {
         assertNotNull(data);
         final Tamm indicator = new Tamm();
         final double actual = indicator.computeDailyValue(fromTestData(data));
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/listener/CompositeIndicatorTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/listener/CompositeIndicatorTest.java
index 7b875fec..06efd484 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/listener/CompositeIndicatorTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/listener/CompositeIndicatorTest.java
@@ -18,7 +18,7 @@
  */
 package fr.inrae.agroclim.indicators.model.indicator.listener;
 
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.Knowledge;
 import fr.inrae.agroclim.indicators.model.indicator.CompositeIndicator;
 import fr.inrae.agroclim.indicators.model.indicator.Indicator;
@@ -39,7 +39,7 @@ public class CompositeIndicatorTest {
     private boolean listenerFired = false;
 
     @Test
-    public void addThenRemove() throws TechnicalException {
+    public void addThenRemove() throws IndicatorsException {
         CompositeIndicator c = new CompositeIndicator();
         c.addFunctionListener((final CompositeIndicator i) -> {
             listenerFired = true;
diff --git a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/listener/PropertyChangeListenerTest.java b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/listener/PropertyChangeListenerTest.java
index 22b835bc..90899b49 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/model/indicator/listener/PropertyChangeListenerTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/model/indicator/listener/PropertyChangeListenerTest.java
@@ -18,7 +18,7 @@
  */
 package fr.inrae.agroclim.indicators.model.indicator.listener;
 
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import fr.inrae.agroclim.indicators.model.Knowledge;
 import fr.inrae.agroclim.indicators.model.criteria.SimpleCriteria;
 import fr.inrae.agroclim.indicators.model.indicator.Indicator;
@@ -51,7 +51,7 @@ public class PropertyChangeListenerTest {
     private IndicatorEvent.Type eventType;
 
     @Test
-    public void cdaystmin() throws TechnicalException {
+    public void cdaystmin() throws IndicatorsException {
         listenerFired = false;
         Knowledge k = Knowledge.load();
 
diff --git a/src/test/java/fr/inrae/agroclim/indicators/xml/XMLUtilTest.java b/src/test/java/fr/inrae/agroclim/indicators/xml/XMLUtilTest.java
index a8597c49..447f6769 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/xml/XMLUtilTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/xml/XMLUtilTest.java
@@ -16,6 +16,7 @@
  */
 package fr.inrae.agroclim.indicators.xml;
 
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
@@ -34,7 +35,6 @@ import java.util.List;
 
 import org.junit.Test;
 
-import fr.inrae.agroclim.indicators.exception.TechnicalException;
 import fr.inrae.agroclim.indicators.model.Evaluation;
 import fr.inrae.agroclim.indicators.model.EvaluationSettings;
 import fr.inrae.agroclim.indicators.model.Knowledge;
@@ -67,7 +67,7 @@ public final class XMLUtilTest extends DataTestHelper {
         try {
             final File xmlFile = getEvaluationTestFile();
             XMLUtil.load(xmlFile, EvaluationSettings.CLASSES_FOR_JAXB);
-        } catch (final TechnicalException e) {
+        } catch (final IndicatorsException e) {
             final StringWriter sw = new StringWriter();
             final PrintWriter pw = new PrintWriter(sw);
             e.printStackTrace(pw);
@@ -172,7 +172,7 @@ public final class XMLUtilTest extends DataTestHelper {
             assertEquals("SoilPhenologyCalculators must be equal",
                     settings.getSoilPhenologyCalculator(), saved.getSoilPhenologyCalculator());
             assertEquals(settings, saved);
-        } catch (final IOException | TechnicalException e) {
+        } catch (final IOException | IndicatorsException e) {
             final StringWriter sw = new StringWriter();
             final PrintWriter pw = new PrintWriter(sw);
             e.printStackTrace(pw);
@@ -192,9 +192,9 @@ public final class XMLUtilTest extends DataTestHelper {
             final File xmlFile = new File(System.getProperty("user.dir")
                     + File.separator + "test/xml/does-not-exist.xml");
             XMLUtil.load(xmlFile, EvaluationSettings.CLASSES_FOR_JAXB);
-        } catch (final TechnicalException e) {
+        } catch (final IndicatorsException e) {
             exception = true;
-            if (e.getRootException() instanceof FileNotFoundException) {
+            if (e.getCause() instanceof FileNotFoundException) {
                 fileNotFoundException = true;
             }
         }
@@ -212,12 +212,12 @@ public final class XMLUtilTest extends DataTestHelper {
             final File xmlFile = getFile("knowledge.xml");
             assertNotNull("knowledge.xml must be found!", xmlFile);
             XMLUtil.load(xmlFile, Knowledge.CLASSES_FOR_JAXB);
-        } catch (final TechnicalException e) {
+        } catch (final IndicatorsException e) {
             LOGGER.catching(e);
             final StringBuilder sb = new StringBuilder();
             sb.append(e.getClass().getCanonicalName()).append(": ");
             sb.append(e.toString());
-            Throwable cause = e.getRootException();
+            Throwable cause = e.getCause();
             while (cause != null) {
                 sb.append(" : ").append(cause.getClass().getCanonicalName())
                 .append(": ").append(cause.getMessage());
@@ -233,11 +233,10 @@ public final class XMLUtilTest extends DataTestHelper {
      * Test saving a new evaluation.
      *
      * @throws java.io.IOException while creating tmp file.
-     * @throws fr.inrae.agroclim.indicators.exception.TechnicalException while
-     * serializing
+     * @throws IndicatorsException while serializing
      */
     @Test
-    public void saveEvaluation() throws IOException, TechnicalException {
+    public void saveEvaluation() throws IOException, IndicatorsException {
         final String name = "évaluation";
         final File clim = getClimate1951File();
         final File phen = getPhenoSampleFile();
-- 
GitLab


From 7ae06fc8a490cafb63d680988572cdcf00fc29c8 Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Thu, 21 Dec 2023 14:08:55 +0100
Subject: [PATCH 04/20] Checkstyle

---
 .../agroclim/indicators/exception/IndicatorsException.java     | 3 ++-
 src/main/java/fr/inrae/agroclim/indicators/xml/XMLUtil.java    | 3 ++-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorsException.java b/src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorsException.java
index 5d143c17..bef29720 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorsException.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorsException.java
@@ -39,6 +39,7 @@ public class IndicatorsException  extends ErrorMessageException {
      *         unknown.)
      */
     public IndicatorsException(final ErrorType errorType, final Throwable cause, final Serializable... arguments) {
-        super(new ErrorMessage("fr.inrae.agroclim.indicators.resources.messages", errorType, List.of(arguments)), cause);
+        super(new ErrorMessage("fr.inrae.agroclim.indicators.resources.messages", errorType, List.of(arguments)),
+                cause);
     }
 }
diff --git a/src/main/java/fr/inrae/agroclim/indicators/xml/XMLUtil.java b/src/main/java/fr/inrae/agroclim/indicators/xml/XMLUtil.java
index 3ad157fc..8dacd948 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/xml/XMLUtil.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/xml/XMLUtil.java
@@ -267,7 +267,8 @@ public abstract class XMLUtil {
      * @throws IndicatorsException
      *             exception from JAXBException
      */
-    public static Object loadResource(final InputStream inputStream, final Class<?>... clazz) throws IndicatorsException {
+    public static Object loadResource(final InputStream inputStream, final Class<?>... clazz)
+            throws IndicatorsException {
         if (inputStream == null) {
             throw new IndicatorsException(XmlErrorType.FILE_NOT_FOUND, "InputStream should not be null!");
         }
-- 
GitLab


From 378e0e16653c028db7c8e2c2e4eca75ff7f7c1f8 Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Thu, 21 Dec 2023 15:13:56 +0100
Subject: [PATCH 05/20] Messages

---
 .gitlab-ci.yml                                | 10 ++-
 .../exception/type/CommonErrorType.java       |  6 +-
 .../exception/type/ResourceErrorType.java     |  6 +-
 .../indicators/model/JEXLFormula.java         | 14 ----
 .../model/indicator/AggregationIndicator.java |  2 +
 .../indicators/resources/messages.properties  | 38 +++++-----
 .../resources/messages_fr.properties          | 71 ++++++++++---------
 7 files changed, 68 insertions(+), 79 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 36caf5f4..474a7029 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -6,9 +6,7 @@ image: registry.forgemia.inra.fr/agroclim/common/docker-projets-java:latest
 stages:
     - build
     - test
-    - checkstyle
-    - pmd
-    - cpd
+    - code-check
     - package
     - deploy
 
@@ -37,17 +35,17 @@ test_job:
         - target/failsafe-reports/TEST-*.xml
 
 checkstyle_job:
-  stage: checkstyle
+  stage: code-check
   script:
     - mvn checkstyle:checkstyle
 
 pmd_job:
-  stage: pmd
+  stage: code-check
   script:
     - mvn pmd:pmd
 
 cpd_job:
-  stage: cpd
+  stage: code-check
   script:
     - mvn pmd:cpd
 
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/type/CommonErrorType.java b/src/main/java/fr/inrae/agroclim/indicators/exception/type/CommonErrorType.java
index 67e89b0e..b500fab0 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/exception/type/CommonErrorType.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/type/CommonErrorType.java
@@ -30,10 +30,8 @@ public interface CommonErrorType extends ErrorType {
         if (!getShortKey().startsWith(cat)) {
             sj.add(cat);
         }
-        if (getParent() != null) {
-            if (!getShortKey().startsWith(getParent().getShortKey())) {
-                sj.add(getParent().getShortKey());
-            }
+        if (getParent() != null && !getShortKey().startsWith(getParent().getShortKey())) {
+            sj.add(getParent().getShortKey());
         }
         sj.add(getShortKey());
         return sj.toString();
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/type/ResourceErrorType.java b/src/main/java/fr/inrae/agroclim/indicators/exception/type/ResourceErrorType.java
index 09631ddb..6be38b76 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/exception/type/ResourceErrorType.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/type/ResourceErrorType.java
@@ -79,16 +79,12 @@ public enum ResourceErrorType implements CommonErrorType {
      * No parameter.
      */
     PHENO_YEARS_EMPTY("211", PHENO_YEARS),
-    /**
-     * Resource in general, topic.
-     */
-    RESOURCES("001", null),
     /**
      * Setting not set.
      *
      * No parameter.
      */
-    RESOURCES_CROPDEVELOPMENT_YEARS("002", RESOURCES),
+    RESOURCES_CROPDEVELOPMENT_YEARS("001", null),
     /**
      * Soil, topic.
      *
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/JEXLFormula.java b/src/main/java/fr/inrae/agroclim/indicators/model/JEXLFormula.java
index 98c5b2be..8085763a 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/JEXLFormula.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/JEXLFormula.java
@@ -20,7 +20,6 @@ package fr.inrae.agroclim.indicators.model;
 
 import java.util.HashSet;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -40,7 +39,6 @@ import fr.inrae.agroclim.indicators.exception.type.ComputationErrorType;
 import fr.inrae.agroclim.indicators.model.criteria.FormulaCriteria;
 import fr.inrae.agroclim.indicators.model.data.Variable;
 import fr.inrae.agroclim.indicators.model.function.aggregation.MathMethod;
-import fr.inrae.agroclim.indicators.resources.I18n;
 import fr.inrae.agroclim.indicators.util.StringUtils;
 import lombok.Getter;
 import lombok.Setter;
@@ -101,18 +99,6 @@ public class JEXLFormula {
                 .create();
     }
 
-    /**
-     * Return error message with inlined arguments.
-     *
-     * @param key message key, without prefix
-     * @param messageArguments arguments for the message.
-     * @return message with arguments
-     */
-    private String errorMessage(final String key, final Object... messageArguments) {
-        final I18n res = new I18n("fr.inrae.agroclim.indicators.resources.messages", Locale.getDefault());
-        return res.format(ERROR_PREFIX + key, messageArguments);
-    }
-
     /**
      * Evaluates the expression with the variables.
      *
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/AggregationIndicator.java b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/AggregationIndicator.java
index 142912ab..33ad49c7 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/indicator/AggregationIndicator.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/indicator/AggregationIndicator.java
@@ -53,6 +53,8 @@ public abstract class AggregationIndicator extends SimpleIndicatorWithCriteria i
 
     /**
      * Override {@link SimpleIndicatorWithCriteria#clone()}.
+     * @return clone
+     * @throws CloneNotSupportedException should not occur
      */
     @Override
     public AggregationIndicator clone() throws CloneNotSupportedException {
diff --git a/src/main/resources/fr/inrae/agroclim/indicators/resources/messages.properties b/src/main/resources/fr/inrae/agroclim/indicators/resources/messages.properties
index dc7ffc32..72ec24e6 100644
--- a/src/main/resources/fr/inrae/agroclim/indicators/resources/messages.properties
+++ b/src/main/resources/fr/inrae/agroclim/indicators/resources/messages.properties
@@ -31,23 +31,23 @@ error.computation.formula.expression.parenthesis=Invalid expression "{0}": missi
 error.computation.formula.expression.parsing=Invalid expression "{0}": parsing error.
 error.computation.formula.function.unknown=Invalid expression "{0}": the function "{1}" is unknown or ambiguous. If the function exists check arguments.
 error.computation.formula.variable.undefined=Invalid expression "{0}": the variable "{1}" is not defined.
-error.computation.input=
-error.computation.input.composite.computation=
-error.computation.input.data.null=
-error.computation.input.quotient.dividend.exception=
-error.computation.input.quotient.divisor.exception=
-error.computation.input.quotient.divisor.zero=
-error.computation.input.variable.value.null=
-error.computation.wrong.definition.criteria.null=
-error.computation.wrong.definition.quotient.dividend.null=
-error.computation.wrong.definition.quotient.divisor.null=
-error.computation.wrong.definition.threshold.null=
-error.computation.wrong.definition.variable.name.null=
+error.computation.input=Indicator computation failed due to an invalid input.
+error.computation.input.composite.computation=An indicator in the CompositeIndicator "{0}" failed to compute.
+error.computation.input.data.null=Daily data must not be null.
+error.computation.input.quotient.dividend.exception=Computation of the dividend indicator "{0}" in Quotient failed.
+error.computation.input.quotient.divisor.exception=Computation of the divisor indicator "{0}" in Quotient failed.
+error.computation.input.quotient.divisor.zero=The result of the divisor indicator "{0}" is zero.
+error.computation.input.variable.value.null=The value for the variable "{0}" must not be null at "{1}".
+error.computation.wrong.definition.criteria.null=The "criteria" property must not be null.
+error.computation.wrong.definition.quotient.dividend.null=Dividend indicator must not be null.
+error.computation.wrong.definition.quotient.divisor.null=Divisor indicator must not be null.
+error.computation.wrong.definition.threshold.null=The threshold must not be null.
+error.computation.wrong.definition.variable.name.null=The variable name must not be null.
 error.day.duplicate={0}: line {1}: this is the same date as previous line "{2}".
 error.day.missing={0}: line {1}: a day is missing before "{2}".
-error.resources.climate=
-error.resources=
-error.resources.cropdevelopment.years=
+error.resources.climate=Error for climate data
+error.resources=Error for resources
+error.resources.cropdevelopment.years=The number of development years for the crop is not set.
 error.resources.soil=Error for soil data!
 error.resources.soil.size.wrong=The number of soil data does not match whole years!
 error.resources.variables=Error for the property ResourceManager.variables!
@@ -65,9 +65,9 @@ error.resources.pheno=Error for phenological data set in resources!
 error.resources.pheno.years.empty=No year in phenological data set in resources!
 error.resources.pheno.years=Error for years in phenological data set in resources!
 error.resources.pheno.years.missing=Phenological data set in resources miss for years {0}!
-error.xml.file.not.found=
-error.xml.unable.to.load=
-error.xml.unable.to.serialize=
+error.xml.file.not.found=The XML file "{0}" was not found.
+error.xml.unable.to.load=XML loading failed.
+error.xml.unable.to.serialize=XML serializing failed.
 error.day.null={0}: line {1}: day is required.
 error.day.succession={0}: line {1}: the day "{2}" is ealier than the day of the previous line "{3}".
 error.date.notread=The start and/or end date could not be read
@@ -90,6 +90,8 @@ warning.tmax.inferiorto.tmin={0}: line {1}: minimal temperature must be inferior
 #eg. CLIMATE: line 0: Climatic variable is missing
 warning.missing={0}: line {1}: {2} is missing
 warning.soilcalculator.4stages=Phenological stages are provided on {0} stages, but SoilCalculator needs 4 stages!
+warning.soilcalculator.4stages[none]=No phenological stage provided whereas soil water balance needs 4 stages!
+warning.soilcalculator.4stages[one]=One phenological stage provided whereas soil water balance needs 4 stages!
 warning.soilloader.missing=No configuration to compute soil water balance or water reserve which are needed for some indicators.
 
 markdown.description.daily=List of available indicators at daily timescale
diff --git a/src/main/resources/fr/inrae/agroclim/indicators/resources/messages_fr.properties b/src/main/resources/fr/inrae/agroclim/indicators/resources/messages_fr.properties
index 090adcc7..b43f027f 100644
--- a/src/main/resources/fr/inrae/agroclim/indicators/resources/messages_fr.properties
+++ b/src/main/resources/fr/inrae/agroclim/indicators/resources/messages_fr.properties
@@ -23,46 +23,50 @@ warning.tmax.inferiorto.tmin={0}\u00a0: ligne {1}\u00a0: la temp\u00e9rature min
 error.climate.dates=Pour l\u2019ann\u00e9e {0}, les dates lues vont du {1} au {2}.
 error.climate.no.data=Aucune donn\u00e9e n\u2019a \u00e9t\u00e9 r\u00e9cup\u00e9r\u00e9e.
 error.climate.wrong.headers=Mauvais nombre d\u2019ent\u00eates\u00a0! {0} ent\u00eates sont dans le fichier : {1}, mais {2} ent\u00eates sont d\u00e9finis : {3}.
-error.computation.formula=Erreur lors de l\u2019ex\u00e9cution de l\u2019expression \u00ab {0} \u00bb.
 error.computation.formula.aggregation.null=L\u2019agr\u00e9gation ne peut \u00eatre nulle.
+error.computation.formula=Erreur lors de l\u2019ex\u00e9cution de l\u2019expression \u00ab {0} \u00bb.
 error.computation.formula.expression.blank=L\u2019expression ne peut \u00eatre vide.
 error.computation.formula.expression.null=L\u2019expression ne peut \u00eatre nulle.
 error.computation.formula.expression.parenthesis=Expression non valide \u00ab {0} \u00bb : des parenth\u00e8ses manquent.
 error.computation.formula.expression.parsing=Expression non valide \u00ab {0} \u00bb : erreur d\u2019analyse.
 error.computation.formula.function.unknown=Expression non valide \u00ab {0} \u00bb : la fonction \u00ab {1} \u00bb est inconnue ou ambig\u00fce. Si la fonction existe, v\u00e9rifiez les arguments.
 error.computation.formula.variable.undefined=Expression non valide \u00ab {0} \u00bb : la variable \u00ab {1} \u00bb n\u2019est pas d\u00e9finie.
-
-error.computation.input=
-error.computation.input.composite.computation=
-error.computation.input.data.null=
-error.computation.input.quotient.dividend.exception=
-error.computation.input.quotient.divisor.exception=
-error.computation.input.quotient.divisor.zero=
-error.computation.input.variable.value.null=
-error.computation.wrong.definition.criteria.null=
-error.computation.wrong.definition.quotient.dividend.null=
-error.computation.wrong.definition.quotient.divisor.null=
-error.computation.wrong.definition.threshold.null=
-error.computation.wrong.definition.variable.name.null=
+error.computation.input=Le calcul de l\u2019indicateur a \u00e9chou\u00e9 \u00e0 cause d\u2019une entr\u00e9e invalide.
+error.computation.input.composite.computation=Le calcul d\u2019un indicateur composant \u00ab {0} \u00bb a \u00e9chou\u00e9.
+error.computation.input.data.null=Les donn\u00e9es journali\u00e8res ne doivent pas \u00eatre nulles.
+error.computation.input.quotient.dividend.exception=Le calcul de l\u2019indicateur num\u00e9rateur \u00ab {0} \u00bb dans Quotient a \u00e9chou\u00e9.
+error.computation.input.quotient.divisor.exception=Le calcul de l\u2019indicateur d\u00e9nominateur \u00ab {0} \u00bb dans Quotient a \u00e9chou\u00e9.
+error.computation.input.quotient.divisor.zero=Le r\u00e9sultat du calcul de l\u2019indicateur d\u00e9nominateur \u00ab {0} \u00bb est z\u00e9ro.
+error.computation.input.variable.value.null=La valeur de la variable \u00ab {0} \u00bb ne doit pas \u00eatre nulle le \u00ab {1} \u00bb.
+error.computation.wrong.definition.criteria.isnt.nocriteria.simplecriteria=criteria n\u2019est ni NoCriteria ni SimpleCriteria\u00a0!
+error.computation.wrong.definition.criteria.null=La propri\u00e9t\u00e9 \u00ab criteria \u00bb ne doit pas \u00eatre nulle.
+error.computation.wrong.definition=L\u2019indicateur \u00ab {0} \u00bb n\u2019est pas bien d\u00e9fini : {1}.
+error.computation.wrong.definition.quotient.dividend.null=L\u2019indicateur num\u00e9rateur ne doit pas \u00eatre nul.
+error.computation.wrong.definition.quotient.divisor.null=L\u2019indicateur d\u00e9nominateur ne doit pas \u00eatre nul.
+error.computation.wrong.definition.threshold.null=Le seuil ne doit pas \u00eatre null.
+error.computation.wrong.definition.variable.name.null=Le nom de la variable ne doit pas \u00eatre nul.
 error.evaluation.resource.climatic=Erreur pour les donn\u00e9es climatiques d\u00e9finies dans les ressources\u00a0!
 error.evaluation.resource=Erreur pour les ressources\u00a0!
-error.evaluation.resource.soil=Erreur pour l donn\u00e9es de sol\u00a0!
-error.evaluation.resource.soil.size.wrong=Le nombre de donn\u00e9es de sol ne correspond pas \u00e0 des ann\u00e9es enti\u00e8res\u00a0!
-error.evaluation.resource.variables.empty=La propri\u00e9t\u00e9 ResourceManager.variables ne doit pas \u00eatre vide\u00a0!
-error.evaluation.resource.variables=Erreur pour la propri\u00e9t\u00e9 ResourceManager.variables\u00a0!
-error.evaluation.resource.variables.missing=La propri\u00e9t\u00e9 ResourceManager.variables ne doit pas \u00eatre nulle\u00a0!
 error.resources.climate.empty=Aucune donn\u00e9e climatique d\u00e9finie dans les ressources\u00a0!
 error.resources.climate.empty.for.phase=Aucune donn\u00e9e climatique pour la phase {0}-{1} (de {2} \u00e0 {3}) en {4}. Donn\u00e9es disponibles entre {5} et {6}.
+error.resources.climate=Erreurs pour les donn\u00e9es climatiques
 error.resources.climate.size.wrong=Le nombre de donn\u00e9es climatiques ne correspond pas \u00e0 des ann\u00e9es enti\u00e8res\u00a0!
 error.resources.climate.years.empty=Aucune ann\u00e9e dans les donn\u00e9es climatiques d\u00e9finies des ressources\u00a0!
-error.resources.climate.years.empty=Erreur pour les ann\u00e9es dans les donn\u00e9es climatiques d\u00e9finies des ressources\u00a0!
+error.resources.climate.years=Erreur pour les ann\u00e9es dans les donn\u00e9es climatiques d\u00e9finies des ressources\u00a0!
 error.resources.climate.years.missing=Les donn\u00e9es climatiques d\u00e9finies dans les ressources manquent pour les ann\u00e9es {0}\u00a0!
+error.resources.cropdevelopment.years=Le nombre d\u2019ann\u00e9es de d\u00e9veloppement de la culture n\u2019est pas d\u00e9fini.
 error.resources.cropdevelopmentyears.null=La propri\u00e9t\u00e9 ResourceManager.cropDevelopmentYears ne doit pas \u00eatre nulle\u00a0!
+error.resources=Erreurs pour les ressources
 error.resources.pheno.empty=Aucune donn\u00e9e ph\u00e9nologique d\u00e9finie dans les ressources\u00a0!
 error.resources.pheno=Erreur pour les donn\u00e9es ph\u00e9nologiques d\u00e9finies dans les ressources\u00a0!
 error.resources.pheno.years.empty=Aucune ann\u00e9e dans les donn\u00e9es ph\u00e9nologiques des ressources\u00a0!
 error.resources.pheno.years=Erreur pour les ann\u00e9es dans les donn\u00e9es ph\u00e9nologiques des ressources\u00a0!
 error.resources.pheno.years.missing=Les donn\u00e9es ph\u00e9nologiques d\u00e9finies dans les ressources manquent pour les ann\u00e9es {0}\u00a0!
+error.resources.soil=Erreurs pour les donn\u00e9es sol
+error.resources.soil.size.wrong=Le nombre de donn\u00e9es de sol ne correspond pas \u00e0 des ann\u00e9es enti\u00e8res\u00a0!
+error.resources.variables.empty=La propri\u00e9t\u00e9 ResourceManager.variables ne doit pas \u00eatre vide\u00a0!
+error.resources.variables=Erreur pour la propri\u00e9t\u00e9 ResourceManager.variables\u00a0!
+error.resources.variables.missing=La propri\u00e9t\u00e9 ResourceManager.variables ne doit pas \u00eatre nulle\u00a0!
 error.rh.outofrange={0}\u00a0: ligne {1}\u00a0: l\u2019humidit\u00e9 relative (%) doit \u00eatre comprise dans l\u2019intervalle 0-100.
 error.title=Erreur
 error.year.null={0}\u00a0: ligne {1}\u00a0: l\u2019ann\u00e9e est obligatoire.
@@ -71,7 +75,7 @@ error.day.duplicate={0}\u00a0: ligne {1}\u00a0: la date est la m\u00eame dans la
 error.day.missing={0}\u00a0: ligne {1}\u00a0: un jour manque avant \u00ab\u00a0{2}\u00a0\u00bb.
 error.day.null={0}\u00a0: ligne {1}\u00a0: le jour est obligatoire.
 error.day.succession={0}\u00a0: ligne {1}\u00a0: le jour \u00ab\u00a0{2}\u00a0\u00bb est ant\u00e9rieur au jour de la ligne pr\u00e9c\u00e9dente \u00ab\u00a0{3}\u00a0\u00bb.
-error.date.notread=La date de d\u00e9but et/ou de fin n'a pas pu \u00eatre lue
+error.date.notread=La date de d\u00e9but et/ou de fin n\u2019a pas pu \u00eatre lue
 error.minimal.stages={0}\u00a0: ligne {1}\u00a0: le fichier ph\u00e9nologique doit contenir au moins deux stades.
 error.climate.jdbc.query=Erreur lors de l\u2019ex\u00e9cution de la requ\u00eate \u00ab\u00a0{0}\u00a0\u00bb\u00a0!
 error.endstage.superiorto.startstage={0}\u00a0: ligne {1}\u00a0: le stade actuel ne peut \u00eatre ant\u00e9rieur au stade suivant.
@@ -113,25 +117,25 @@ EvaluationType.WITH_AGGREGATION.name=avec agr\u00e9gation
 EvaluationType.WITHOUT_AGGREGATION.name=sans agr\u00e9gation
 
 MathMethod.avg.description = Renvoie la moyenne des valeurs pass\u00e9es en param\u00e8tre de la fonction.
-MathMethod.exp.description = Renvoie le nombre Euler e \u00e9lev\u00e9 \u00e0 la puissance d'une valeur double. Cas sp\u00e9ciaux :\n\n\
-Si l'argument est NaN, le r\u00e9sultat est NaN.\n\
-Si l'argument in l'infini positif, le r\u00e9sultat est l'infini positif.\n\
-Si l'argument in l'infini n\u00e9gatif, le r\u00e9sultat est z\u00e9ro positif.\n\
+MathMethod.exp.description = Renvoie le nombre Euler e \u00e9lev\u00e9 \u00e0 la puissance d\u2019une valeur double. Cas sp\u00e9ciaux :\n\n\
+Si l\u2019argument est NaN, le r\u00e9sultat est NaN.\n\
+Si l\u2019argument in l\u2019infini positif, le r\u00e9sultat est l\u2019infini positif.\n\
+Si l\u2019argument in l\u2019infini n\u00e9gatif, le r\u00e9sultat est z\u00e9ro positif.\n\
 The computed result must be within 1 ulp of the exact result. Results must be semi-monotonic.
 MathMethod.log.description=Returns the natural logarithm (base e) of a double value. Special cases:\n\n\
-Si l'argument est NaN ou n\u00e9gatif, le r\u00e9sultat est NaN.\n\
-Si l'argument in l'infini positif, le r\u00e9sultat est l'infini positif.\n\
-Si l'argument est z\u00e9ro (positif ou n\u00e9gatif), le r\u00e9sultat est l'infini n\u00e9gatif.\n\n
+Si l\u2019argument est NaN ou n\u00e9gatif, le r\u00e9sultat est NaN.\n\
+Si l\u2019argument in l\u2019infini positif, le r\u00e9sultat est l\u2019infini positif.\n\
+Si l\u2019argument est z\u00e9ro (positif ou n\u00e9gatif), le r\u00e9sultat est l\u2019infini n\u00e9gatif.\n\n
 The computed result must be within 1 ulp of the exact result. Results must be semi-monotonic.
 MathMethod.max.description = Renvoie la plus grande des valeurs.\n\n\
-C'est-\u00e0-dire que le r\u00e9sultat est l'argument le plus proche de l'infini positif.\n\
+C\u2019est-\u00e0-dire que le r\u00e9sultat est l\u2019argument le plus proche de l\u2019infini positif.\n\
 Si les arguments ont la m\u00eame valeur, le r\u00e9sultat est cette m\u00eame valeur.\n\
-Si l'une des valeurs est NaN, le r\u00e9sultat est NaN.\n\
+Si l\u2019une des valeurs est NaN, le r\u00e9sultat est NaN.\n\
 La fonction prend un ou plusieurs arguments en entr\u00e9e.
 MathMethod.min.description = Retourner la plus petite des valeurs.\n\n\
-C'est-\u00e0-dire que le r\u00e9sultat est la valeur la plus proche de l'infini n\u00e9gatif.\n\
+C\u2019est-\u00e0-dire que le r\u00e9sultat est la valeur la plus proche de l\u2019infini n\u00e9gatif.\n\
 Si les arguments ont la m\u00eame valeur, le r\u00e9sultat est cette m\u00eame valeur.\n\
-Si l'une des valeurs est NaN, le r\u00e9sultat est NaN.\n\
+Si l\u2019une des valeurs est NaN, le r\u00e9sultat est NaN.\n\
 La fonction prend un ou plusieurs arguments en entr\u00e9e.
 PhenologicalModelType.curve.name=curvilin\u00e9aire
 PhenologicalModelType.curve_grapevine.name=curvilin\u00e9aire vigne
@@ -152,3 +156,6 @@ Variable.RH.description=Humidit\u00e9 relative [%].
 Variable.SOILWATERCONTENT.description=Teneur en eau du sol [% massique].
 Variable.WATER_RESERVE.description=R\u00e9serve en eau du sol [mm].
 Variable.WIND.description=Vitesse du vent [m/s].
+error.xml.file.not.found=Le fichier XML \u00ab {0} \u00bb n\u2019a pas \u00e9t\u00e9 trouv\u00e9.
+error.xml.unable.to.load=Le chargement du fichier XML a \u00e9chou\u00e9.
+error.xml.unable.to.serialize=La s\u00e9rialisation du fichier XML a \u00e9chou\u00e9.
-- 
GitLab


From f85ba87a8ca5c88b659ece5aa041f84d2827f344 Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Thu, 21 Dec 2023 15:37:53 +0100
Subject: [PATCH 06/20] 1.3.0-SNAPSHOT

---
 CITATION.cff   | 4 ++--
 codemeta.json  | 2 +-
 pom.xml        | 2 +-
 publiccode.yml | 4 ++--
 4 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/CITATION.cff b/CITATION.cff
index fee74dc4..68322cdb 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -13,8 +13,8 @@ keywords:
 - Java
 - library
 - indicators
-version: 1.2.5-SNAPSHOT
+version: 1.3.0-SNAPSHOT
 doi: 10.15454/IZUFAP
-date-released: 2023-12-19
+date-released: 2023-12-21
 license: GPL-3.0
 repository-code: https://forgemia.inra.fr/agroclim/Indicators/indicators-java.git
diff --git a/codemeta.json b/codemeta.json
index a5f97b95..1429d446 100644
--- a/codemeta.json
+++ b/codemeta.json
@@ -99,5 +99,5 @@
             "name": "lombok"
         }
     ],
-    "version": "1.2.5-SNAPSHOT"
+    "version": "1.3.0-SNAPSHOT"
 }
diff --git a/pom.xml b/pom.xml
index d6e135a2..b20bf8e8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,7 +22,7 @@ along with Indicators. If not, see <https://www.gnu.org/licenses/>.
     <name>Indicators</name>
     <description>Library of agro- and eco-climatic indicators.</description>
     <inceptionYear>2018</inceptionYear>
-    <version>1.2.5-SNAPSHOT</version>
+    <version>1.3.0-SNAPSHOT</version>
     <packaging>jar</packaging>
     <licenses>
         <license>
diff --git a/publiccode.yml b/publiccode.yml
index c7537c5b..3cc75557 100644
--- a/publiccode.yml
+++ b/publiccode.yml
@@ -49,9 +49,9 @@ maintenance:
     name: "J\xE9r\xE9mie D\xE9c\xF4me"
   type: internal
 name: Indicators
-releaseDate: 2023-12-19
+releaseDate: 2023-12-21
 softwareType: library
-softwareVersion: 1.2.5-SNAPSHOT
+softwareVersion: 1.3.0-SNAPSHOT
 url: https://forgemia.inra.fr/agroclim/Indicators/indicators-java.git
 usedBy:
 - INRAE AgroClim
-- 
GitLab


From c44a9b20fcdd393f1acc9d43248f8923e530826f Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Wed, 3 Jan 2024 14:57:57 +0100
Subject: [PATCH 07/20] PMD

---
 .../java/fr/inrae/agroclim/indicators/model/JEXLFormula.java | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/JEXLFormula.java b/src/main/java/fr/inrae/agroclim/indicators/model/JEXLFormula.java
index 8085763a..4eb2e1ef 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/JEXLFormula.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/JEXLFormula.java
@@ -54,11 +54,6 @@ import org.apache.commons.jexl3.introspection.JexlPermissions;
  */
 public class JEXLFormula {
 
-    /**
-     * I18N key prefix for error messages.
-     */
-    private static final String ERROR_PREFIX = JEXLFormula.class.getSimpleName() + ".error.";
-
     /**
      * org.apache.commons.jexl3.JexlEngine .
      */
-- 
GitLab


From dbd53987d5d6e60d3c1d9575ef33693ebc0a362d Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Wed, 3 Jan 2024 16:02:30 +0100
Subject: [PATCH 08/20] .gitlab-ci.yml : `needs`

---
 .gitlab-ci.yml | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 474a7029..75197667 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -24,6 +24,7 @@ build_job:
 
 test_job:
   stage: test
+  needs: ["build_job"]
   script:
     - echo "Maven test started"
     - mvn test
@@ -36,16 +37,19 @@ test_job:
 
 checkstyle_job:
   stage: code-check
+  needs: ["build_job"]
   script:
     - mvn checkstyle:checkstyle
 
 pmd_job:
   stage: code-check
+  needs: ["build_job"]
   script:
     - mvn pmd:pmd
 
 cpd_job:
   stage: code-check
+  needs: ["build_job"]
   script:
     - mvn pmd:cpd
 
-- 
GitLab


From 62f3500c86ca44890bdd6902fa1893090dc8921a Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Wed, 3 Jan 2024 16:29:24 +0100
Subject: [PATCH 09/20] Factoriser pour tester ErrorType

---
 .../indicators/util/ErrorTypeUtils.java       | 60 +++++++++++++++++++
 .../indicators/exception/ErrorTypeTest.java   | 38 ++----------
 2 files changed, 65 insertions(+), 33 deletions(-)
 create mode 100644 src/main/java/fr/inrae/agroclim/indicators/util/ErrorTypeUtils.java

diff --git a/src/main/java/fr/inrae/agroclim/indicators/util/ErrorTypeUtils.java b/src/main/java/fr/inrae/agroclim/indicators/util/ErrorTypeUtils.java
new file mode 100644
index 00000000..443a9d90
--- /dev/null
+++ b/src/main/java/fr/inrae/agroclim/indicators/util/ErrorTypeUtils.java
@@ -0,0 +1,60 @@
+package fr.inrae.agroclim.indicators.util;
+
+import fr.inrae.agroclim.indicators.exception.ErrorType;
+import fr.inrae.agroclim.indicators.resources.I18n;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Helper methods to ensure all implementations of {@link ErrorType} are well defined.
+ *
+ * @author Olivier Maury
+ */
+public interface ErrorTypeUtils {
+
+    static Set<String> getDuplicatedCodes(final Class<? extends Enum<?>> clazz) {
+        final Set<String> duplicates = new HashSet<>();
+        final Set<String> codes = new HashSet<>();
+        final Enum<?>[] values = clazz.getEnumConstants();
+        for (final Enum<?> value : values) {
+            final ErrorType k = (ErrorType) value;
+            if (codes.contains(k.getSubCode())) {
+                duplicates.add(k.getSubCode());
+            }
+            codes.add(k.getSubCode());
+        }
+        return duplicates;
+    }
+
+    /**
+     * Get missing translations for the Enum in the bundle for the locales.
+     *
+     * @param clazz      enum class
+     * @param bundleName bundle name for the Properties file.
+     * @param locales    locales of translations
+     * @return missing translations
+     */
+    static Map<Locale, Map<Class<?>, List<String>>> getMissingTranslations(final Class<? extends Enum<?>> clazz,
+            final String bundleName, final List<Locale> locales) {
+        final Map<Locale, Map<Class<?>, List<String>>> missing = new HashMap<>();
+        for (final Locale locale : locales) {
+            final I18n i18n = new I18n(bundleName, locale);
+            final Enum<?>[] values = clazz.getEnumConstants();
+            for (final Enum<?> value : values) {
+                final ErrorType k = (ErrorType) value;
+                final String tr = i18n.get(k.getI18nKey());
+                if (tr.startsWith("!") && tr.endsWith("!")) {
+                    missing.computeIfAbsent(locale, l -> new HashMap<>());
+                    missing.get(locale).computeIfAbsent(clazz, l -> new ArrayList<>());
+                    missing.get(locale).get(clazz).add(k.getI18nKey());
+                }
+            }
+        }
+        return missing;
+    }
+}
diff --git a/src/test/java/fr/inrae/agroclim/indicators/exception/ErrorTypeTest.java b/src/test/java/fr/inrae/agroclim/indicators/exception/ErrorTypeTest.java
index 863d0896..dfed45e2 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/exception/ErrorTypeTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/exception/ErrorTypeTest.java
@@ -5,10 +5,6 @@ import fr.inrae.agroclim.indicators.exception.type.ResourceErrorType;
 import fr.inrae.agroclim.indicators.exception.type.XmlErrorType;
 import static org.junit.Assert.assertTrue;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -18,15 +14,12 @@ import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
-import fr.inrae.agroclim.indicators.resources.I18n;
+import fr.inrae.agroclim.indicators.util.ErrorTypeUtils;
 
 /**
  * Ensure all implementations of {@link ErrorType} are well defined.
  *
- * Last changed : $Date: 2023-03-16 17:36:45 +0100 (jeu. 16 mars 2023) $
- *
- * @author $Author: omaury $
- * @version $Revision: 644 $
+ * @author Olivier Maury
  */
 @RunWith(Parameterized.class)
 public class ErrorTypeTest {
@@ -59,35 +52,14 @@ public class ErrorTypeTest {
 
     @Test
     public void i18n() {
-        final Map<Locale, Map<Class<?>, List<String>>> missing = new HashMap<>();
-        for (final Locale locale : Arrays.asList(Locale.ENGLISH, Locale.FRENCH)) {
-            final I18n i18n = new I18n(BUNDLE_NAME, locale);
-            final Enum<?>[] values = clazz.getEnumConstants();
-            for (final Enum<?> value : values) {
-                final ErrorType k = (ErrorType) value;
-                final String tr = i18n.get(k.getI18nKey());
-                if (tr.startsWith("!") && tr.endsWith("!")) {
-                    missing.computeIfAbsent(locale, l -> new HashMap<>());
-                    missing.get(locale).computeIfAbsent(clazz, l -> new ArrayList<>());
-                    missing.get(locale).get(clazz).add(k.getI18nKey());
-                }
-            }
-        }
+        final Map<Locale, Map<Class<?>, List<String>>> missing = ErrorTypeUtils.getMissingTranslations(clazz,
+                BUNDLE_NAME, List.of(Locale.ENGLISH, Locale.FRENCH));
         assertTrue(missing.toString(), missing.isEmpty());
     }
 
     @Test
     public void subCodeAreUnique() {
-        final Set<String> codes = new HashSet<>();
-        final Set<String> duplicates = new HashSet<>();
-        final Enum<?>[] values = clazz.getEnumConstants();
-        for (final Enum<?> value : values) {
-            final ErrorType k = (ErrorType) value;
-            if (codes.contains(k.getSubCode())) {
-                duplicates.add(k.getSubCode());
-            }
-            codes.add(k.getSubCode());
-        }
+        final Set<String> duplicates = ErrorTypeUtils.getDuplicatedCodes(clazz);
         assertTrue(duplicates.isEmpty());
     }
 }
-- 
GitLab


From 7fc224876173c354002c70704096f7ec7c753e9e Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Thu, 4 Jan 2024 12:20:26 +0100
Subject: [PATCH 10/20] Lister les codes d'erreurs dans la documentation

---
 .gitlab-ci.yml                                |  14 +
 README.md                                     |   2 +-
 pom.xml                                       |  27 +-
 .../agroclim/indicators/GenerateMarkdown.java | 390 ++++++++++++++++++
 .../exception/type/CommonErrorType.java       |   6 +
 .../agroclim/indicators/model/Knowledge.java  | 266 +-----------
 .../indicators/resources/messages.properties  |  10 +
 .../resources/messages_fr.properties          |  42 +-
 src/site/en/markdown/.gitignore               |   3 +
 src/site/en/markdown/index.md.vm              |  59 +++
 src/site/markdown/.gitignore                  |   5 +
 src/site/markdown/index.md.vm                 |  16 +-
 src/site/site.xml                             |   4 +
 src/site/site_en.xml                          |  38 ++
 .../IndicatorsErrorCategoryTest.java          |  60 +++
 15 files changed, 651 insertions(+), 291 deletions(-)
 create mode 100644 src/main/java/fr/inrae/agroclim/indicators/GenerateMarkdown.java
 create mode 100644 src/site/en/markdown/.gitignore
 create mode 100644 src/site/en/markdown/index.md.vm
 create mode 100644 src/site/markdown/.gitignore
 create mode 100644 src/site/site_en.xml
 create mode 100644 src/test/java/fr/inrae/agroclim/indicators/exception/IndicatorsErrorCategoryTest.java

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 75197667..61201565 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -9,6 +9,7 @@ stages:
     - code-check
     - package
     - deploy
+    - pages
 
 cache:
   paths:
@@ -65,3 +66,16 @@ deploy_job:
     - main
   script:
     - echo "Maven deploy started"
+
+pages_job:
+  stage: deploy
+  script:
+    - mvn exec:java -Dexec.mainClass=fr.inrae.agroclim.indicators.GenerateMarkdown -Dexec.args='${basedir}/src/site/markdown -'
+    - mv src/site/markdown/*-en.md src/site/en/markdown/
+    - mvn site
+    - mv target/site public
+  artifacts:
+    paths:
+    - public
+  only:
+  - tags
diff --git a/README.md b/README.md
index 2b748cc0..f614643f 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ The library is used in the free software [GETARI](https://agroclim.inrae.fr/geta
 
 ## 🧐 Features
 
-It contains predefined indicators : 91 daily and 48 hourly.
+It contains predefined indicators : 89 daily and 48 hourly.
 
 ## 🛠️ Tech Stack
 
diff --git a/pom.xml b/pom.xml
index b20bf8e8..fe9f90b4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -391,12 +391,23 @@ along with Indicators. If not, see <https://www.gnu.org/licenses/>.
                     <artifactId>maven-deploy-plugin</artifactId>
                     <version>2.8.2</version>
                 </plugin>
+                <plugin>
+                    <artifactId>maven-project-info-reports-plugin</artifactId>
+                    <version>3.5.0</version>
+                </plugin>
                 <plugin>
                     <artifactId>maven-site-plugin</artifactId>
                     <version>3.12.1</version>
                     <configuration>
-                        <locales>fr</locales>
+                        <locales>fr,en</locales>
                     </configuration>
+                    <dependencies>
+                      <dependency>
+                        <groupId>org.apache.maven.doxia</groupId>
+                        <artifactId>doxia-module-markdown</artifactId>
+                        <version>2.0.0-M7</version>
+                      </dependency>
+                    </dependencies>
                 </plugin>
                 <!-- -->
                 <plugin>
@@ -517,6 +528,20 @@ along with Indicators. If not, see <https://www.gnu.org/licenses/>.
                         <sourceFileExclude>module-info.*</sourceFileExclude>
                     </sourceFileExcludes>
                 </configuration>
+                <reportSets>
+                    <reportSet>
+                        <id>default</id>
+                        <reports>
+                            <report>javadoc</report>
+                        </reports>
+                    </reportSet>
+                    <reportSet>
+                        <id>aggregate</id>
+                        <reports>
+                            <report>aggregate</report>
+                        </reports>
+                    </reportSet>
+                </reportSets>
             </plugin>
 
             <!-- Include JaCoCo report into site -->
diff --git a/src/main/java/fr/inrae/agroclim/indicators/GenerateMarkdown.java b/src/main/java/fr/inrae/agroclim/indicators/GenerateMarkdown.java
new file mode 100644
index 00000000..6303dd3f
--- /dev/null
+++ b/src/main/java/fr/inrae/agroclim/indicators/GenerateMarkdown.java
@@ -0,0 +1,390 @@
+package fr.inrae.agroclim.indicators;
+
+import fr.inrae.agroclim.indicators.exception.IndicatorsException;
+import fr.inrae.agroclim.indicators.exception.type.CommonErrorType;
+import fr.inrae.agroclim.indicators.exception.type.ComputationErrorType;
+import fr.inrae.agroclim.indicators.exception.type.ResourceErrorType;
+import fr.inrae.agroclim.indicators.exception.type.XmlErrorType;
+import fr.inrae.agroclim.indicators.model.Knowledge;
+import fr.inrae.agroclim.indicators.model.LocalizedString;
+import fr.inrae.agroclim.indicators.model.Note;
+import fr.inrae.agroclim.indicators.model.Parameter;
+import fr.inrae.agroclim.indicators.model.TimeScale;
+import fr.inrae.agroclim.indicators.model.indicator.CompositeIndicator;
+import fr.inrae.agroclim.indicators.model.indicator.Indicator;
+import fr.inrae.agroclim.indicators.resources.I18n;
+import fr.inrae.agroclim.indicators.util.StringUtils;
+import fr.inrae.agroclim.indicators.util.Utf8BufferedWriter;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+
+/**
+ * Utility to create Markdown and CSV files for Hugo and Maven site.
+ *
+ * @author Olivier Maury
+ */
+@Log4j2
+@RequiredArgsConstructor
+public class GenerateMarkdown {
+
+    /**
+     * Mardown Yaml front matter.
+     */
+    private static final String FRONT_MATTER = """
+                                       ---
+                                       title: %s
+                                       description: %s
+                                       keywords: %s
+                                       date: %s
+                                       ---
+
+                                       """;
+    /**
+     * Creation date for front matter.
+     */
+    private String created;
+
+    /**
+     * Separator between file base name and language code.
+     */
+    private final String languageSep;
+    /**
+     * The directory where Markdown files are generated.
+     */
+    private final String outDir;
+
+    /**
+     * @param args arguments : outDir, languageSep
+     * @throws IndicatorsException while loading knowledge
+     * @throws java.io.IOException while writing file
+     */
+    public static void main(final String[] args) throws IndicatorsException, IOException {
+        LOGGER.traceEntry("Arguments: {}", Arrays.asList(args));
+        final String outDir;
+        final String languageSep;
+        if (args != null && args.length > 0) {
+            outDir = args[0];
+            if (args.length > 1) {
+                languageSep = args[1];
+            } else {
+                languageSep = ".";
+            }
+        } else {
+            outDir = System.getProperty("java.io.tmpdir");
+            languageSep = ".";
+        }
+        var instance = new GenerateMarkdown(languageSep, outDir);
+        instance.run();
+    }
+
+    private void run() throws IndicatorsException, IOException {
+        final DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
+        created = df.format(new Date());
+
+        for (final Locale l : Arrays.asList(Locale.ENGLISH, Locale.FRENCH)) {
+            writeErrorMdFile(l);
+        }
+        for (final TimeScale timescale : TimeScale.values()) {
+            LOGGER.trace("Generating files for {}...", timescale);
+            final Knowledge knowledge = Knowledge.load(timescale);
+            for (final Locale l : Arrays.asList(Locale.ENGLISH, Locale.FRENCH)) {
+                writeIndicatorsMdFiles(knowledge, timescale, l);
+            }
+            writeIndicatorsCsvFiles(knowledge, timescale);
+            writeParametersCsvFiles(knowledge, timescale);
+            LOGGER.trace("Generating files for {}... done", timescale);
+        }
+        // TODO error codes
+    }
+
+    /**
+     * Write the Markdown file showing all error codes and descriptions.
+     *
+     * @param locale locale for the strings
+     * @throws IOException file not found or error while writting
+     */
+    private void writeErrorMdFile(final Locale locale) throws IOException {
+        final String languageCode = locale.getLanguage();
+        final Path path = Paths.get(outDir, "errors" + languageSep + languageCode + ".md");
+        final String bundleName = "fr.inrae.agroclim.indicators.resources.messages";
+        final I18n i18n = new I18n(bundleName, locale);
+        LOGGER.trace(path);
+        try (BufferedWriter writer = new Utf8BufferedWriter(path)) {
+            writer.write(String.format(FRONT_MATTER,
+                    i18n.get("markdown.error.title"),
+                    i18n.get("markdown.error.description"),
+                    i18n.get("markdown.error.keywords"),
+                    created));
+            writeLn(writer, i18n.get("markdown.error.fullcode"), i18n.get("markdown.error.name"),
+                    i18n.get("markdown.error.message"));
+            writer.write("|:----------|:-----------|:-----------|\n");
+            final List<CommonErrorType> types = new ArrayList<>();
+            types.addAll(Arrays.asList(XmlErrorType.values()));
+            types.addAll(Arrays.asList(ResourceErrorType.values()));
+            types.addAll(Arrays.asList(ComputationErrorType.values()));
+            String previousCat = "";
+            for (final CommonErrorType type : types) {
+                var cat = type.getCategory().getCategory(i18n);
+                if (!previousCat.equals(cat)) {
+                    var catCode = type.getCategory().getCode();
+                    writeLn(writer, "**" + i18n.get("markdown.error.category") + " `" + catCode + "` - " + cat + "**");
+                    previousCat = cat;
+                }
+                var fullCode = type.getFullCode();
+                var name = type.getName();
+                var description = i18n.get(type.getI18nKey());
+                writeLn(writer, fullCode, name, description);
+            }
+        }
+    }
+
+    /**
+     * @param knowledge knowledge to export as files
+     * @param timescale timescale of related indicators
+     * @throws IOException
+     */
+    private void writeIndicatorsCsvFiles(final Knowledge knowledge, final TimeScale timescale) throws IOException {
+        final String suffix = "-" + timescale.name().toLowerCase();
+        final Path path = Paths.get(outDir, "indicators" + suffix + ".csv");
+        LOGGER.trace(path);
+        try (BufferedWriter writer = new Utf8BufferedWriter(path)) {
+            writer.write("id;nom_en;nom_fr;description_en;description_fr;variables;param\u00e8tres;notes\n");
+            for (final CompositeIndicator comp : knowledge.getIndicators()) {
+                for (final Indicator ind : comp.getIndicators()) {
+                    writer.write(ind.getId());
+                    writer.write(";");
+                    writer.write(ind.getName("en"));
+                    writer.write(";");
+                    if (!ind.getName("fr").equals(ind.getName("en"))) {
+                        writer.write(ind.getName("fr"));
+                    }
+                    writer.write(";");
+                    final String description = ind.getDescription("fr");
+                    if (!description.equals(ind.getDescription("en"))) {
+                        writer.write(ind.getDescription("en"));
+                    }
+                    writer.write(";");
+                    writer.write(description);
+                    writer.write(";");
+                    final List<String> variables = new LinkedList<>();
+                    if (ind.getVariables() != null && !ind.getVariables().isEmpty()) {
+                        ind.getVariables().forEach(variable -> variables.add(variable.getName()));
+                        Collections.sort(variables);
+                        writer.write(String.join(", ", variables));
+                    }
+                    writer.write(";");
+                    final List<String> parameters = new LinkedList<>();
+                    if (ind.getParameters() != null
+                            && !ind.getParameters().isEmpty()) {
+                        ind.getParameters().forEach(param -> parameters.add(param.getId()));
+                        Collections.sort(parameters);
+                        writer.write(String.join(", ", parameters));
+                    }
+                    // affichage des références des notes de l'indicateurs
+                    writer.write(";");
+                    final List<String> notes = new LinkedList<>();
+                    if (ind.getNotes() != null && !ind.getNotes().isEmpty()) {
+                        ind.getNotes().forEach(note -> notes.add(note.getId()));
+                        writer.write(String.join(", ", notes));
+                    }
+                    writer.write("\n");
+                }
+            }
+        }
+    }
+
+    /**
+     * @param knowledge knowledge to export as files
+     * @param timescale timescale of related indicators
+     * @param locale    locale to write
+     * @throws IOException
+     */
+    private void writeIndicatorsMdFiles(final Knowledge knowledge, final TimeScale timescale, final Locale locale)
+            throws IOException {
+        final String languageCode = locale.getLanguage();
+        final String suffix = "-" + timescale.name().toLowerCase();
+        final Path mdPath = Paths.get(outDir, "indicators" + suffix + languageSep + languageCode + ".md");
+        final String bundleName = "fr.inrae.agroclim.indicators.resources.messages";
+        final I18n i18n = new I18n(bundleName, locale);
+
+        LOGGER.trace(mdPath);
+        try (BufferedWriter mdWriter = new Utf8BufferedWriter(mdPath);) {
+            final long nb = knowledge.getIndicators().stream().mapToInt(comp -> comp.getIndicators().size()).sum();
+            final String indicatorsVersion = fr.inrae.agroclim.indicators.resources.Version.getString("version");
+            final String frontMatter = """
+                                       ---
+                                       title: %s
+                                       description: %s
+                                       keywords: %s
+                                       date: %s
+                                       ---
+
+                                       """;
+            mdWriter.write(String.format(frontMatter,
+                    i18n.get("markdown.title." + timescale.name().toLowerCase()),
+                    i18n.get("markdown.description." + timescale.name().toLowerCase()),
+                    i18n.get("markdown.keywords"),
+                    created));
+            mdWriter.write(i18n.format("markdown.indicators.version", indicatorsVersion) + "\n\n"
+                    + "## " + i18n.format("markdown.indicators." + timescale.name().toLowerCase(), nb) + "\n");
+            writeLn(mdWriter, i18n.get("markdown.id"), i18n.get("markdown.name"), i18n.get("markdown.description"),
+                    i18n.get("markdown.variables"), i18n.get("markdown.parameters"),
+                    i18n.get("markdown.unit") + " [^1]", i18n.get("markdown.notes"));
+            mdWriter.write("|:---|:-----|:------------|:----------|:-----------|:-----------|:-----------|\n");
+
+            final Set<String> allVariables = new HashSet<>();
+            for (final CompositeIndicator comp : knowledge.getIndicators()) {
+                writeLn(mdWriter, "**" + comp.getName(languageCode) + "**");
+                for (final Indicator ind : comp.getIndicators()) {
+                    final List<String> variables = new LinkedList<>();
+                    if (ind.getVariables() != null
+                            && !ind.getVariables().isEmpty()) {
+                        ind.getVariables().forEach(variable -> variables.add(variable.getName()));
+                        Collections.sort(variables);
+                        allVariables.addAll(variables);
+                    }
+                    final List<String> parameters = new LinkedList<>();
+                    if (ind.getParameters() != null
+                            && !ind.getParameters().isEmpty()) {
+                        ind.getParameters().forEach(param -> parameters.add(param.getId()));
+                        Collections.sort(parameters);
+                    }
+                    String unit = "";
+                    if (ind.getUnit() != null) {
+                        List<LocalizedString> symbols = ind.getUnit().getSymbols();
+                        if (symbols != null && !symbols.isEmpty()) {
+                            unit = LocalizedString.getString(symbols, languageCode);
+                        }
+                        if (unit == null || unit.isBlank()) {
+                            final List<LocalizedString> labels = ind.getUnit().getLabels();
+                            if (labels != null && !labels.isEmpty()) {
+                                unit = LocalizedString.getString(labels, languageCode);
+                            }
+                        }
+                    }
+                    // affichage des références des notes de l'indicateurs
+                    final List<String> notes = new LinkedList<>();
+                    if (ind.getNotes() != null && !ind.getNotes().isEmpty()) {
+                        ind.getNotes().forEach(note -> {
+                            final String anchor = note.getId();
+                            notes.add("<a href='#" + anchor + "'>" + note.getId() + "</a>");
+                        });
+                    }
+                    writeLn(mdWriter, ind.getId(), ind.getName(languageCode),
+                            ind.getDescription(languageCode), String.join(", ", variables),
+                            String.join(", ", parameters), unit, String.join(", ", notes));
+                }
+            }
+
+            mdWriter.write("""
+
+                           ###\s""" + i18n.get("markdown.parameters") + "\n"
+                    + "| " + i18n.get("markdown.id") + " | " + i18n.get("markdown.description") + " |\n"
+                    + "|:---|:------------|\n");
+
+            for (final Parameter param : knowledge.getParameters()) {
+                writeLn(mdWriter, param.getId(), param.getDescription(languageCode));
+            }
+
+            mdWriter.write("""
+
+                           ###\s""" + i18n.get("markdown.variables") + "\n"
+                    + "| " + i18n.get("markdown.id") + " | " + i18n.get("markdown.description") + " |\n"
+                    + "|:---|:------------|\n");
+            allVariables.stream().sorted().forEach(variable -> {
+                try {
+                    mdWriter.write("| ");
+                    mdWriter.write(variable);
+                    mdWriter.write(" | ");
+                    mdWriter.write(i18n.get("Variable." + variable.toUpperCase() + ".description"));
+                    mdWriter.write(" |\n");
+                } catch (final IOException ex) {
+                    LOGGER.catching(ex);
+                }
+            });
+
+            // Ecriture de l'ensemble des notes présentes
+            if (knowledge.getNotes() != null && !knowledge.getNotes().isEmpty()) {
+                mdWriter.write("""
+
+                               ###\s""" + i18n.get("markdown.notes") + "\n"
+                        + "| " + i18n.get("markdown.reference") + " | " + i18n.get("markdown.description") + " |\n"
+                        + "|:---|:------------|\n");
+                for (final Note note : knowledge.getNotes()) {
+                    final String anchor;
+
+                    // si il s'agit d'un DOI, on affiche le lien
+                    final String id = note.getId();
+                    if (StringUtils.isDoiRef(id)) {
+                        anchor = String.format(
+                                "<a id=\"%1$s\" href=\"https://doi.org/%1$s\" target=\"_blank\">%1$s</a>",
+                                id);
+                    } else {
+                        anchor = String.format("<a id=\"%1$s\">%1$s</a>", id);
+                    }
+
+                    writeLn(mdWriter, anchor, note.getDescription());
+                }
+            }
+
+            mdWriter.write("\n\n[^1]: " + i18n.get("markdown.unit.footnote"));
+        }
+    }
+
+    /**
+     * Write a line in a table.
+     *
+     * @param writer the writer to user
+     * @param values strings to write
+     * @throws IOException when using BufferedWriter.write
+     */
+    private static void writeLn(final BufferedWriter writer,
+            final String... values) throws IOException {
+        final int nb = values.length;
+        writer.write("| ");
+        for (int i = 0; i < nb; i++) {
+            writer.write(values[i]);
+            if (i < nb - 1) {
+                writer.write(" | ");
+            }
+        }
+        writer.write(" |\n");
+    }
+
+    /**
+     * @param knowledge knowledge to export as files
+     * @param timescale timescale of related indicators
+     * @throws IOException
+     */
+    private void writeParametersCsvFiles(final Knowledge knowledge, final TimeScale timescale) throws IOException {
+        final String suffix = "-" + timescale.name().toLowerCase();
+        final Path paramPath = Paths.get(outDir, "parameters" + suffix + ".csv");
+        LOGGER.trace(paramPath);
+        try (BufferedWriter paramWriter = new Utf8BufferedWriter(paramPath)) {
+            paramWriter.write("id;description_fr;description_en\n");
+            for (final Parameter param : knowledge.getParameters()) {
+                paramWriter.write(param.getId());
+                paramWriter.write(";");
+                paramWriter.write(param.getDescription("fr"));
+                paramWriter.write(";");
+                paramWriter.write(param.getDescription("en"));
+                paramWriter.write("\n");
+            }
+        }
+    }
+}
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/type/CommonErrorType.java b/src/main/java/fr/inrae/agroclim/indicators/exception/type/CommonErrorType.java
index b500fab0..8db6e449 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/exception/type/CommonErrorType.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/type/CommonErrorType.java
@@ -16,8 +16,14 @@ public interface CommonErrorType extends ErrorType {
         return getName().toLowerCase().replace("_", ".");
     }
 
+    /**
+     * @return parent error
+     */
     CommonErrorType getParent();
 
+    /**
+     * @return error name (as {@link Enum#name()).
+     */
     String name();
 
     /**
diff --git a/src/main/java/fr/inrae/agroclim/indicators/model/Knowledge.java b/src/main/java/fr/inrae/agroclim/indicators/model/Knowledge.java
index 741c8993..637484e0 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/model/Knowledge.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/model/Knowledge.java
@@ -17,24 +17,11 @@
 package fr.inrae.agroclim.indicators.model;
 
 import fr.inrae.agroclim.indicators.exception.IndicatorsException;
-import java.io.BufferedWriter;
-import java.io.IOException;
 import java.io.InputStream;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Date;
 import java.util.EnumMap;
-import java.util.HashSet;
-import java.util.LinkedList;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
-import java.util.Set;
 
 import javax.xml.bind.annotation.XmlAccessType;
 import javax.xml.bind.annotation.XmlAccessorType;
@@ -75,9 +62,6 @@ import fr.inrae.agroclim.indicators.model.indicator.Quotient;
 import fr.inrae.agroclim.indicators.model.indicator.SimpleIndicator;
 import fr.inrae.agroclim.indicators.model.indicator.Sum;
 import fr.inrae.agroclim.indicators.model.indicator.Tamm;
-import fr.inrae.agroclim.indicators.resources.I18n;
-import fr.inrae.agroclim.indicators.util.StringUtils;
-import fr.inrae.agroclim.indicators.util.Utf8BufferedWriter;
 import fr.inrae.agroclim.indicators.xml.XMLUtil;
 import javax.xml.bind.annotation.XmlAttribute;
 import lombok.Getter;
@@ -89,10 +73,7 @@ import lombok.extern.log4j.Log4j2;
 /**
  * knowledge.xml.
  *
- * Last change $Date$
- *
- * @author $Author$
- * @version $Revision$
+ * @author Olivier Maury
  */
 @XmlRootElement
 @XmlAccessorType(XmlAccessType.FIELD)
@@ -196,251 +177,6 @@ public final class Knowledge implements Cloneable {
         return load(stream);
     }
 
-    /**
-     * @param args not used
-     * @throws IndicatorsException while loading knowledge
-     * @throws java.io.IOException while writing file
-     */
-    public static void main(final String[] args) throws IndicatorsException,
-    IOException {
-        for (final TimeScale timescale: TimeScale.values()) {
-            LOGGER.trace("Generating files for {}...", timescale);
-            final Knowledge knowledge = Knowledge.load(timescale);
-            for (final Locale l: Arrays.asList(Locale.ENGLISH, Locale.FRENCH)) {
-                writeMarkDownFiles(knowledge, timescale, l);
-            }
-            writeFiles(knowledge, timescale);
-            LOGGER.trace("Generating files for {}... done", timescale);
-        }
-    }
-    /**
-     * @param knowledge knowledge to export as files
-     * @param timescale timescale of related indicators
-     * @throws IOException
-     */
-    private static void writeFiles(final Knowledge knowledge, final TimeScale timescale) throws IOException {
-        final String suffix = "-" + timescale.name().toLowerCase();
-        final String tmpDir = System.getProperty("java.io.tmpdir");
-        final Path path = Paths.get(tmpDir, "indicators" + suffix + ".csv");
-        final Path paramPath = Paths.get(tmpDir, "parameters" + suffix + ".csv");
-        LOGGER.trace(path);
-        LOGGER.trace(paramPath);
-        try (
-                BufferedWriter writer = new Utf8BufferedWriter(path);
-                BufferedWriter paramWriter = new Utf8BufferedWriter(paramPath);) {
-            writer.write("""
-                         id;nom_en;nom_fr;description_en;description_fr;variables;param\u00e8tres;notes
-                         """);
-            for (final CompositeIndicator comp : knowledge.getIndicators()) {
-                for (final Indicator ind : comp.getIndicators()) {
-                    writer.write(ind.getId());
-                    writer.write(";");
-                    writer.write(ind.getName("en"));
-                    writer.write(";");
-                    if (!ind.getName("fr").equals(ind.getName("en"))) {
-                        writer.write(ind.getName("fr"));
-                    }
-                    writer.write(";");
-                    final String description = ind.getDescription("fr");
-                    if (!description.equals(ind.getDescription("en"))) {
-                        writer.write(ind.getDescription("en"));
-                    }
-                    writer.write(";");
-                    writer.write(description);
-                    writer.write(";");
-                    final List<String> variables = new LinkedList<>();
-                    if (ind.getVariables() != null
-                            && !ind.getVariables().isEmpty()) {
-                        ind.getVariables().forEach(variable -> variables.add(variable.getName()));
-                        Collections.sort(variables);
-                        writer.write(String.join(", ", variables));
-                    }
-                    writer.write(";");
-                    final List<String> parameters = new LinkedList<>();
-                    if (ind.getParameters() != null
-                            && !ind.getParameters().isEmpty()) {
-                        ind.getParameters().forEach(param -> parameters.add(param.getId()));
-                        Collections.sort(parameters);
-                        writer.write(String.join(", ", parameters));
-                    }
-                    // affichage des références des notes de l'indicateurs
-                    writer.write(";");
-                    final List<String> notes = new LinkedList<>();
-                    if (ind.getNotes() != null && !ind.getNotes().isEmpty()) {
-                        ind.getNotes().forEach(note -> notes.add(note.getId()));
-                        writer.write(String.join(", ", notes));
-                    }
-                    writer.write("\n");
-                }
-            }
-            paramWriter.write("id;description_fr;description_en\n");
-            for (final Parameter param : knowledge.getParameters()) {
-                paramWriter.write(param.getId());
-                paramWriter.write(";");
-                paramWriter.write(param.getDescription("fr"));
-                paramWriter.write(";");
-                paramWriter.write(param.getDescription("en"));
-                paramWriter.write("\n");
-            }
-        }
-    }
-
-    /**
-     * Write a line.
-     *
-     * @param writer the writer to user
-     * @param values strings to write
-     * @throws IOException when using BufferedWriter.write
-     */
-    private static void writeLn(final BufferedWriter writer,
-            final String... values) throws IOException {
-        final int nb = values.length;
-        writer.write("| ");
-        for (int i = 0; i < nb; i++) {
-            writer.write(values[i]);
-            if (i < nb - 1) {
-                writer.write(" | ");
-            }
-        }
-        writer.write(" |\n");
-    }
-
-    /**
-     * @param knowledge knowledge to export as files
-     * @param timescale timescale of related indicators
-     * @param locale locale to write
-     * @throws IOException
-     */
-    private static void writeMarkDownFiles(final Knowledge knowledge, final TimeScale timescale, final Locale locale)
-            throws IOException {
-        final String languageCode = locale.getLanguage();
-        final String suffix = "-" + timescale.name().toLowerCase();
-        final String tmpDir = System.getProperty("java.io.tmpdir");
-        final Path mdPath = Paths.get(tmpDir, "indicators" + suffix + "." + languageCode + ".md");
-        final String bundleName = "fr.inrae.agroclim.indicators.resources.messages";
-        final I18n i18n = new I18n(bundleName, locale);
-
-        final DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
-        final String created = df.format(new Date());
-
-        LOGGER.trace(mdPath);
-        try (BufferedWriter mdWriter = new Utf8BufferedWriter(mdPath);) {
-            final long nb = knowledge.getIndicators().stream().mapToInt(comp -> comp.getIndicators().size()).sum();
-            final String indicatorsVersion = fr.inrae.agroclim.indicators.resources.Version.getString("version");
-            mdWriter.write("+++\n"
-                    + "# $Id: $\n"
-                    + "title = \"" + i18n.get("markdown.title." + timescale.name().toLowerCase()) + "\"\n"
-                    + "description = \"" + i18n.get("markdown.description." + timescale.name().toLowerCase()) + "\"\n"
-                    + "keywords = [" + i18n.get("markdown.keywords") + "]\n"
-                    + "date = " + created + "\n"
-                    + "+++\n"
-                    + i18n.format("markdown.indicators.version", indicatorsVersion) + "\n\n"
-                    + "## " + i18n.format("markdown.indicators." + timescale.name().toLowerCase(), nb) + "\n"
-                    + "| " + String.join(" | ",
-                            i18n.get("markdown.id"), i18n.get("markdown.name"), i18n.get("markdown.description"),
-                            i18n.get("markdown.variables"), i18n.get("markdown.parameters"),
-                            i18n.get("markdown.unit") + " [^1]", i18n.get("markdown.notes")) + " |\n"
-                            + "|:---|:-----|:------------|:----------|:-----------|:-----------|:-----------|\n");
-
-            final Set<String> allVariables = new HashSet<>();
-            for (final CompositeIndicator comp : knowledge.getIndicators()) {
-                writeLn(mdWriter, "**" + comp.getName(languageCode) + "**");
-                for (final Indicator ind : comp.getIndicators()) {
-                    final List<String> variables = new LinkedList<>();
-                    if (ind.getVariables() != null
-                            && !ind.getVariables().isEmpty()) {
-                        ind.getVariables().forEach(variable -> variables.add(variable.getName()));
-                        Collections.sort(variables);
-                        allVariables.addAll(variables);
-                    }
-                    final List<String> parameters = new LinkedList<>();
-                    if (ind.getParameters() != null
-                            && !ind.getParameters().isEmpty()) {
-                        ind.getParameters().forEach(param -> parameters.add(param.getId()));
-                        Collections.sort(parameters);
-                    }
-                    String unit = "";
-                    if (ind.getUnit() != null) {
-                        List<LocalizedString> symbols = ind.getUnit().getSymbols();
-                        if (symbols != null && !symbols.isEmpty()) {
-                            unit = LocalizedString.getString(symbols, languageCode);
-                        }
-                        if (unit == null || unit.isBlank()) {
-                            final List<LocalizedString> labels = ind.getUnit().getLabels();
-                            if (labels != null && !labels.isEmpty()) {
-                                unit = LocalizedString.getString(labels, languageCode);
-                            }
-                        }
-                    }
-                    // affichage des références des notes de l'indicateurs
-                    final List<String> notes = new LinkedList<>();
-                    if (ind.getNotes() != null && !ind.getNotes().isEmpty()) {
-                        ind.getNotes().forEach(note -> {
-                            final String anchor = note.getId();
-                            notes.add("<a href='#" + anchor + "'>" + note.getId() + "</a>");
-                        });
-                    }
-                    writeLn(mdWriter, ind.getId(), ind.getName(languageCode),
-                            ind.getDescription(languageCode), String.join(", ", variables),
-                            String.join(", ", parameters), unit, String.join(", ", notes));
-                }
-            }
-
-            mdWriter.write("""
-
-                           ###\s""" + i18n.get("markdown.parameters") + "\n"
-                    + "| " + i18n.get("markdown.id") + " | " + i18n.get("markdown.description") + " |\n"
-                    + "|:---|:------------|\n");
-
-            for (final Parameter param : knowledge.getParameters()) {
-                writeLn(mdWriter, param.getId(), param.getDescription(languageCode));
-            }
-
-            mdWriter.write("""
-
-                           ###\s""" + i18n.get("markdown.variables") + "\n"
-                    + "| " + i18n.get("markdown.id") + " | " + i18n.get("markdown.description") + " |\n"
-                    + "|:---|:------------|\n");
-            allVariables.stream().sorted().forEach(variable -> {
-                try {
-                    mdWriter.write("| ");
-                    mdWriter.write(variable);
-                    mdWriter.write(" | ");
-                    mdWriter.write(i18n.get("Variable." + variable.toUpperCase() + ".description"));
-                    mdWriter.write(" |\n");
-                } catch (final IOException ex) {
-                    LOGGER.catching(ex);
-                }
-            });
-
-            // Ecriture de l'ensemble des notes présentes
-            if (knowledge.getNotes() != null && !knowledge.getNotes().isEmpty()) {
-                mdWriter.write("""
-
-                               ###\s""" + i18n.get("markdown.notes") + "\n"
-                        + "| " + i18n.get("markdown.reference") + " | " + i18n.get("markdown.description") + " |\n"
-                        + "|:---|:------------|\n");
-                for (final Note note : knowledge.getNotes()) {
-                    final String anchor;
-
-                    // si il s'agit d'un DOI, on affiche le lien
-                    final String id = note.getId();
-                    if (StringUtils.isDoiRef(id)) {
-                        anchor = String.format(
-                                "<a id=\"%1$s\" href=\"https://doi.org/%1$s\" target=\"_blank\">%1$s</a>",
-                                id);
-                    } else {
-                        anchor = String.format("<a id=\"%1$s\">%1$s</a>", id);
-                    }
-
-                    writeLn(mdWriter, anchor, note.getDescription());
-                }
-            }
-
-            mdWriter.write("\n\n[^1]: " + i18n.get("markdown.unit.footnote"));
-        }
-    }
-
     /**
      * Indicators for cultural practices.
      */
diff --git a/src/main/resources/fr/inrae/agroclim/indicators/resources/messages.properties b/src/main/resources/fr/inrae/agroclim/indicators/resources/messages.properties
index 72ec24e6..441aca5a 100644
--- a/src/main/resources/fr/inrae/agroclim/indicators/resources/messages.properties
+++ b/src/main/resources/fr/inrae/agroclim/indicators/resources/messages.properties
@@ -15,6 +15,9 @@
 # along with Indicators. If not, see <https://www.gnu.org/licenses/>.
 #
 #Errors
+error.category.computation=Evaluation computation
+error.category.resources=Evaluation resources
+error.category.xml=XML parsing
 error.climate.jdbc.query=Error while query execution "{0}"!
 error.climate.dates=For the year {0}, available dates are from {1} to {2}.
 error.climate.no.data=No data were retrieved.
@@ -99,6 +102,13 @@ markdown.description.hourly=List of available indicators at hourly timescale
 markdown.title.daily=Indicators at daily timescale
 markdown.title.hourly=Indicators at hourly timescale
 markdown.keywords="indicator","agroclimatic","ecoclimatic"
+markdown.error.category=Category
+markdown.error.description=List of error types: codes and descriptions.
+markdown.error.fullcode=Error code
+markdown.error.keywords="library","error codes"
+markdown.error.message=Error message
+markdown.error.name=Error name
+markdown.error.title=Error types
 markdown.indicators=Indicators
 markdown.indicators.daily={0} daily indicators
 markdown.indicators.hourly={0} hourly indicators
diff --git a/src/main/resources/fr/inrae/agroclim/indicators/resources/messages_fr.properties b/src/main/resources/fr/inrae/agroclim/indicators/resources/messages_fr.properties
index b43f027f..afbe1374 100644
--- a/src/main/resources/fr/inrae/agroclim/indicators/resources/messages_fr.properties
+++ b/src/main/resources/fr/inrae/agroclim/indicators/resources/messages_fr.properties
@@ -20,27 +20,30 @@ warning.soilcalculator.4stages[none]=Aucun stade ph\u00e9nologique n\u2019est fo
 warning.soilcalculator.4stages[one]=Un stade ph\u00e9nologique est fourni, mais le calcul du bilan hydrique en n\u00e9cessite 4\u00a0!
 warning.soilloader.missing=La configuration pour le calcul du bilan hydrique manque alors que des indicateurs portent sur la teneur en eau du sol ou la r\u00e9serve utile.
 warning.tmax.inferiorto.tmin={0}\u00a0: ligne {1}\u00a0: la temp\u00e9rature minimale doit \u00eatre inf\u00e9rieure \u00e0 la temp\u00e9rature maximale.
+error.category.computation=Calcul de l\u2019\u00e9valuation
+error.category.resources=Ressources de l\u2019\u00e9valuation
+error.category.xml=Lecture du fichier XML
 error.climate.dates=Pour l\u2019ann\u00e9e {0}, les dates lues vont du {1} au {2}.
 error.climate.no.data=Aucune donn\u00e9e n\u2019a \u00e9t\u00e9 r\u00e9cup\u00e9r\u00e9e.
-error.climate.wrong.headers=Mauvais nombre d\u2019ent\u00eates\u00a0! {0} ent\u00eates sont dans le fichier : {1}, mais {2} ent\u00eates sont d\u00e9finis : {3}.
+error.climate.wrong.headers=Mauvais nombre d\u2019ent\u00eates\u00a0! {0} ent\u00eates sont dans le fichier\u00a0: {1}, mais {2} ent\u00eates sont d\u00e9finis\u00a0: {3}.
 error.computation.formula.aggregation.null=L\u2019agr\u00e9gation ne peut \u00eatre nulle.
-error.computation.formula=Erreur lors de l\u2019ex\u00e9cution de l\u2019expression \u00ab {0} \u00bb.
+error.computation.formula=Erreur lors de l\u2019ex\u00e9cution de l\u2019expression \u00ab\u00a0{0}\u00a0\u00bb.
 error.computation.formula.expression.blank=L\u2019expression ne peut \u00eatre vide.
 error.computation.formula.expression.null=L\u2019expression ne peut \u00eatre nulle.
-error.computation.formula.expression.parenthesis=Expression non valide \u00ab {0} \u00bb : des parenth\u00e8ses manquent.
-error.computation.formula.expression.parsing=Expression non valide \u00ab {0} \u00bb : erreur d\u2019analyse.
-error.computation.formula.function.unknown=Expression non valide \u00ab {0} \u00bb : la fonction \u00ab {1} \u00bb est inconnue ou ambig\u00fce. Si la fonction existe, v\u00e9rifiez les arguments.
-error.computation.formula.variable.undefined=Expression non valide \u00ab {0} \u00bb : la variable \u00ab {1} \u00bb n\u2019est pas d\u00e9finie.
+error.computation.formula.expression.parenthesis=Expression non valide \u00ab\u00a0{0}\u00a0\u00bb\u00a0: des parenth\u00e8ses manquent.
+error.computation.formula.expression.parsing=Expression non valide \u00ab\u00a0{0}\u00a0\u00bb\u00a0: erreur d\u2019analyse.
+error.computation.formula.function.unknown=Expression non valide \u00ab\u00a0{0}\u00a0\u00bb\u00a0: la fonction \u00ab\u00a0{1}\u00a0\u00bb est inconnue ou ambig\u00fce. Si la fonction existe, v\u00e9rifiez les arguments.
+error.computation.formula.variable.undefined=Expression non valide \u00ab\u00a0{0}\u00a0\u00bb\u00a0: la variable \u00ab\u00a0{1}\u00a0\u00bb n\u2019est pas d\u00e9finie.
 error.computation.input=Le calcul de l\u2019indicateur a \u00e9chou\u00e9 \u00e0 cause d\u2019une entr\u00e9e invalide.
-error.computation.input.composite.computation=Le calcul d\u2019un indicateur composant \u00ab {0} \u00bb a \u00e9chou\u00e9.
+error.computation.input.composite.computation=Le calcul d\u2019un indicateur composant \u00ab\u00a0{0}\u00a0\u00bb a \u00e9chou\u00e9.
 error.computation.input.data.null=Les donn\u00e9es journali\u00e8res ne doivent pas \u00eatre nulles.
-error.computation.input.quotient.dividend.exception=Le calcul de l\u2019indicateur num\u00e9rateur \u00ab {0} \u00bb dans Quotient a \u00e9chou\u00e9.
-error.computation.input.quotient.divisor.exception=Le calcul de l\u2019indicateur d\u00e9nominateur \u00ab {0} \u00bb dans Quotient a \u00e9chou\u00e9.
-error.computation.input.quotient.divisor.zero=Le r\u00e9sultat du calcul de l\u2019indicateur d\u00e9nominateur \u00ab {0} \u00bb est z\u00e9ro.
-error.computation.input.variable.value.null=La valeur de la variable \u00ab {0} \u00bb ne doit pas \u00eatre nulle le \u00ab {1} \u00bb.
-error.computation.wrong.definition.criteria.isnt.nocriteria.simplecriteria=criteria n\u2019est ni NoCriteria ni SimpleCriteria\u00a0!
-error.computation.wrong.definition.criteria.null=La propri\u00e9t\u00e9 \u00ab criteria \u00bb ne doit pas \u00eatre nulle.
-error.computation.wrong.definition=L\u2019indicateur \u00ab {0} \u00bb n\u2019est pas bien d\u00e9fini : {1}.
+error.computation.input.quotient.dividend.exception=Le calcul de l\u2019indicateur num\u00e9rateur \u00ab\u00a0{0}\u00a0\u00bb dans Quotient a \u00e9chou\u00e9.
+error.computation.input.quotient.divisor.exception=Le calcul de l\u2019indicateur d\u00e9nominateur \u00ab\u00a0{0}\u00a0\u00bb dans Quotient a \u00e9chou\u00e9.
+error.computation.input.quotient.divisor.zero=Le r\u00e9sultat du calcul de l\u2019indicateur d\u00e9nominateur \u00ab\u00a0{0}\u00a0\u00bb est z\u00e9ro.
+error.computation.input.variable.value.null=La valeur de la variable \u00ab\u00a0{0}\u00a0\u00bb ne doit pas \u00eatre nulle pour le jour \u00ab\u00a0{1}\u00a0\u00bb.
+error.computation.wrong.definition.criteria.isnt.nocriteria.simplecriteria=La propri\u00e9t\u00e9 \u00ab\u00a0criteria\u00a0\u00bb n\u2019est ni NoCriteria ni SimpleCriteria\u00a0!
+error.computation.wrong.definition.criteria.null=La propri\u00e9t\u00e9 \u00ab\u00a0criteria\u00a0\u00bb ne doit pas \u00eatre nulle.
+error.computation.wrong.definition=L\u2019indicateur \u00ab\u00a0{0}\u00a0\u00bb n\u2019est pas bien d\u00e9fini\u00a0: {1}.
 error.computation.wrong.definition.quotient.dividend.null=L\u2019indicateur num\u00e9rateur ne doit pas \u00eatre nul.
 error.computation.wrong.definition.quotient.divisor.null=L\u2019indicateur d\u00e9nominateur ne doit pas \u00eatre nul.
 error.computation.wrong.definition.threshold.null=Le seuil ne doit pas \u00eatre null.
@@ -92,6 +95,13 @@ markdown.description.hourly=Liste des indicateurs disponibles au pas de temps ho
 markdown.title.daily=Indicateurs au pas de temps journalier
 markdown.title.hourly=Indicateurs au pas de temps horaire
 markdown.keywords="indicateur","agroclimatique","\u00e9coclimatique"
+markdown.error.category=Cat\u00e9gorie
+markdown.error.description=Liste des erreurs\u00a0: codes et descriptions.
+markdown.error.fullcode=Code d\u2019erreur
+markdown.error.keywords="biblioth\u00e8que","code d\u2019erreur"
+markdown.error.message=Message d\u2019erreur
+markdown.error.name=Nom de l\u2019erreur
+markdown.error.title=Codes d\u2019erreurs
 markdown.indicators=Indicateurs
 markdown.indicators.daily={0} indicateurs journaliers
 markdown.indicators.hourly={0} indicateurs horaires
@@ -117,7 +127,7 @@ EvaluationType.WITH_AGGREGATION.name=avec agr\u00e9gation
 EvaluationType.WITHOUT_AGGREGATION.name=sans agr\u00e9gation
 
 MathMethod.avg.description = Renvoie la moyenne des valeurs pass\u00e9es en param\u00e8tre de la fonction.
-MathMethod.exp.description = Renvoie le nombre Euler e \u00e9lev\u00e9 \u00e0 la puissance d\u2019une valeur double. Cas sp\u00e9ciaux :\n\n\
+MathMethod.exp.description = Renvoie le nombre Euler e \u00e9lev\u00e9 \u00e0 la puissance d\u2019une valeur double. Cas sp\u00e9ciaux\u00a0:\n\n\
 Si l\u2019argument est NaN, le r\u00e9sultat est NaN.\n\
 Si l\u2019argument in l\u2019infini positif, le r\u00e9sultat est l\u2019infini positif.\n\
 Si l\u2019argument in l\u2019infini n\u00e9gatif, le r\u00e9sultat est z\u00e9ro positif.\n\
@@ -156,6 +166,6 @@ Variable.RH.description=Humidit\u00e9 relative [%].
 Variable.SOILWATERCONTENT.description=Teneur en eau du sol [% massique].
 Variable.WATER_RESERVE.description=R\u00e9serve en eau du sol [mm].
 Variable.WIND.description=Vitesse du vent [m/s].
-error.xml.file.not.found=Le fichier XML \u00ab {0} \u00bb n\u2019a pas \u00e9t\u00e9 trouv\u00e9.
+error.xml.file.not.found=Le fichier XML \u00ab\u00a0{0}\u00a0\u00bb n\u2019a pas \u00e9t\u00e9 trouv\u00e9.
 error.xml.unable.to.load=Le chargement du fichier XML a \u00e9chou\u00e9.
 error.xml.unable.to.serialize=La s\u00e9rialisation du fichier XML a \u00e9chou\u00e9.
diff --git a/src/site/en/markdown/.gitignore b/src/site/en/markdown/.gitignore
new file mode 100644
index 00000000..62c71ef0
--- /dev/null
+++ b/src/site/en/markdown/.gitignore
@@ -0,0 +1,3 @@
+errors-en.md
+indicators-daily-en.md
+indicators-hourly-en.md
diff --git a/src/site/en/markdown/index.md.vm b/src/site/en/markdown/index.md.vm
new file mode 100644
index 00000000..262e036e
--- /dev/null
+++ b/src/site/en/markdown/index.md.vm
@@ -0,0 +1,59 @@
+#set($h1 = '#')
+#set($h2 = '##')
+#set($h3 = '###')
+#set($h4 = '####')
+<head>
+    <title>Technical documenation for de ${project.name}</title>
+</head>
+
+$h1 Technical documentation for ${project.name}
+
+Indicator library, usable in eco-climatic mode (with an embeddded phenological model) as well as agro-climatic.
+This library is used by the desktop software [GETARI](https://agroclim.inrae.fr/getari/en/) and the workflow SEASON embeeded into [SICLIMA](https://agroclim.inrae.fr/siclima/).
+
+$h2 Project links:
+
+- [Project info](project-info.html): information about code management, issues and bugs managnements, and continuous integration server.
+- [Dependencies](dependencies.html).
+- [Project license](license.html): from the tag `<licenses>` in the file `pom.xml`.
+
+$h2 Code structuration
+
+Source code is divided as:
+
+- `pom.xml`: Maven file
+- `src/main/` : source code
+- `src/test/` : source code for tests
+- `src/site/` : documentation, see below
+
+$h3 Documentation
+
+Documentation is writtent in the folder `/src/site/markdown/`.
+Site structure is defined in `/src/site/site_en.xml`.
+Plain text formats are prefered to allow SCM.
+This document is writtent in Markdown format.
+
+UML diagrams are written in [PlantUML](http://plantuml.com/) format
+(extension `.puml` in `/src/site/resources/images/`).
+The prefixes for the diagrams:
+
+- `act-` : activity diagrams
+- `cas-` : use case diagrams
+- `cls-` : class diagrams
+- `cmp-` : component diagrams
+- `seq-` : sequence diagrams
+
+The technical documentation is generated by Maven.
+The Markdown files showing indicators and error codes are generated by the class `GenerateMarkdown`.
+
+```
+mvn exec:java -Dexec.mainClass=fr.inrae.agroclim.indicators.GenerateMarkdown -Dexec.args='${basedir}/src/site/markdown -'
+mvn site
+```
+
+Jetty can be used for real time rendering,
+so refreshing the browser shows the changes: use `mvn site:run` and visit the link,
+usually http://localhost:8080/.
+
+If needed, generate the UML diagrams using
+`mvn com.github.jeluard:plantuml-maven-plugin:generate`.
diff --git a/src/site/markdown/.gitignore b/src/site/markdown/.gitignore
new file mode 100644
index 00000000..e533f96d
--- /dev/null
+++ b/src/site/markdown/.gitignore
@@ -0,0 +1,5 @@
+errors-*.md
+indicators-daily*
+indicators-hourly*
+parameters-daily.csv
+parameters-hourly.csv
diff --git a/src/site/markdown/index.md.vm b/src/site/markdown/index.md.vm
index 535955ae..071b957d 100644
--- a/src/site/markdown/index.md.vm
+++ b/src/site/markdown/index.md.vm
@@ -8,7 +8,7 @@
 
 $h1 Documentation technique de ${project.name}
 
-Bibliothèque d'indicateurs, utilisable autant en mode éco-climatique (avec modèle phénologique intégré) qu'enen mode agro-climatique.
+Bibliothèque d'indicateurs, utilisable autant en mode éco-climatique (avec modèle phénologique intégré) qu'en mode agro-climatique.
 Cette bibliothèque est utilisée dans le logiciel de bureau [GETARI](https://agroclim.inrae.fr/getari/) et la chaîne de traitement SEASON intégrée dans [SICLIMA](https://agroclim.inrae.fr/siclima/).
 
 $h2 Liens du projet :
@@ -43,7 +43,13 @@ Voici les préfixes pour les diagrammes :
 - `cmp-` : diagrammes de composants
 - `seq-` : diagrammes de séquence
 
-La documentation technique est générée par Maven avec la commande `mvn site`.
+La documentation technique est générée par Maven.
+Les fichiers Markdown listant les indicateurs et les codes d'erreur sont générés par la classe `GenerateMarkdown`.
+
+```
+mvn exec:java -Dexec.mainClass=fr.inrae.agroclim.indicators.GenerateMarkdown -Dexec.args='${basedir}/src/site/markdown -'
+mvn site
+```
 
 Jetty peut être utilisé pour le rendu en temps réel,
 c'est-à-dire qu'il suffit de rafraîchir le navigateur pour afficher
@@ -52,9 +58,3 @@ généralement http://localhost:8080/.
 
 Au besoin, regénérer les diagrammes UML avec la commande
 `mvn com.github.jeluard:plantuml-maven-plugin:generate`.
-
-Les fichiers Markdown listant les indicateurs sont générés par la classe `Knowlegde` que l'on peut exécuter avec Maven :
-`mvn exec:java -Dexec.mainClass="fr.inrae.agroclim.indicators.model.Knowledge"`.
-----
-
-`$Id$`
diff --git a/src/site/site.xml b/src/site/site.xml
index 3059eae5..f0bb9bd0 100644
--- a/src/site/site.xml
+++ b/src/site/site.xml
@@ -26,6 +26,10 @@
     <body>
         <menu name="Documentation" inherit="top">
             <item name="Accueil" href="index.html" />
+            <item name="Indicateurs horaires" href="indicators-hourly-fr.html" />
+            <item name="Indicateurs journaliers" href="indicators-daily-fr.html" />
+            <item name="Codes d'erreurs" href="errors-fr.html" />
+            <item name="Documentation in English" href="en/index.html" />
         </menu>
         <menu ref="modules" inherit="top" />
         <menu ref="reports" inherit="top" />
diff --git a/src/site/site_en.xml b/src/site/site_en.xml
new file mode 100644
index 00000000..ce0af405
--- /dev/null
+++ b/src/site/site_en.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE xml>
+<!-- Last changed : $Date$ -->
+<!-- @author $Author$ -->
+<!-- @version $Revision$ -->
+<project name="${project.name}">
+    <skin>
+        <groupId>org.apache.maven.skins</groupId>
+        <artifactId>maven-fluido-skin</artifactId>
+        <version>1.9</version>
+    </skin>
+
+    <custom>
+        <fluidoSkin>
+            <sideBarEnabled>true</sideBarEnabled>
+            <sourceLineNumbersEnabled>true</sourceLineNumbersEnabled>
+        </fluidoSkin>
+    </custom>
+
+    <bannerLeft>
+        <name>Documentation for ${project.name}</name>
+    </bannerLeft>
+
+    <version position="bottom" />
+
+    <body>
+        <menu name="Documentation" inherit="top">
+            <item name="Home" href="index.html" />
+            <item name="Hourly indicators" href="indicators-hourly-en.html" />
+            <item name="Daily indicators" href="indicators-daily-en.html" />
+            <item name="Error codes" href="errors-en.html" />
+            <item name="Documentation en français" href="../index.html" />
+        </menu>
+        <menu ref="modules" inherit="top" />
+        <menu ref="reports" inherit="top" />
+        <menu ref="parent" inherit="top" />
+    </body>
+</project>
\ No newline at end of file
diff --git a/src/test/java/fr/inrae/agroclim/indicators/exception/IndicatorsErrorCategoryTest.java b/src/test/java/fr/inrae/agroclim/indicators/exception/IndicatorsErrorCategoryTest.java
new file mode 100644
index 00000000..25ea12db
--- /dev/null
+++ b/src/test/java/fr/inrae/agroclim/indicators/exception/IndicatorsErrorCategoryTest.java
@@ -0,0 +1,60 @@
+package fr.inrae.agroclim.indicators.exception;
+
+import fr.inrae.agroclim.indicators.resources.I18n;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.Test;
+import static org.junit.Assert.assertFalse;
+
+/**
+ * Ensure all translations of {@link IndicatorsErrorCategory} are well defined.
+ *
+ * @author Olivier Maury
+ */
+@RunWith(Parameterized.class)
+public class IndicatorsErrorCategoryTest {
+
+    /**
+     * @return classes to test.
+     */
+    @Parameterized.Parameters
+    public static List<IndicatorsErrorCategory> data() {
+        return Arrays.asList(IndicatorsErrorCategory.values());
+    }
+
+    /**
+     * Category to test.
+     */
+    private final IndicatorsErrorCategory category;
+
+    /**
+     * Constructor.
+     *
+     * @param cat category to test
+     */
+    public IndicatorsErrorCategoryTest(final IndicatorsErrorCategory cat) {
+        this.category = cat;
+    }
+
+    private void missingTranslation(Locale locale) {
+        final String bundleName = "fr.inrae.agroclim.indicators.resources.messages";
+        final I18n i18n = new I18n(bundleName, locale);
+        final String tr = category.getCategory(i18n);
+        final boolean actual = tr.startsWith("!") && tr.endsWith("!");
+        final String msg = tr + " is not translated in " + locale;
+        assertFalse(msg, actual);
+    }
+
+    @Test
+    public void missingTranslationEnglish() {
+        missingTranslation(Locale.ENGLISH);
+    }
+
+    @Test
+    public void missingTranslationFrench() {
+        missingTranslation(Locale.FRENCH);
+    }
+}
-- 
GitLab


From 825f0261f2979dd2c23653fd6e1734a2b5fb2468 Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Thu, 4 Jan 2024 12:30:05 +0100
Subject: [PATCH 11/20] Test GitLab pages

---
 .gitlab-ci.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 61201565..45bc65be 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -69,7 +69,9 @@ deploy_job:
 
 pages_job:
   stage: deploy
+  needs: ["package_job"]
   script:
+    - mvn --version
     - mvn exec:java -Dexec.mainClass=fr.inrae.agroclim.indicators.GenerateMarkdown -Dexec.args='${basedir}/src/site/markdown -'
     - mv src/site/markdown/*-en.md src/site/en/markdown/
     - mvn site
@@ -77,5 +79,3 @@ pages_job:
   artifacts:
     paths:
     - public
-  only:
-  - tags
-- 
GitLab


From 2a20e1c7ebb894d5fdc7967fb8a019213563d213 Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Thu, 4 Jan 2024 14:08:47 +0100
Subject: [PATCH 12/20] Test GitLab pages

---
 .gitlab-ci.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 45bc65be..7806ab01 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -72,7 +72,7 @@ pages_job:
   needs: ["package_job"]
   script:
     - mvn --version
-    - mvn exec:java -Dexec.mainClass=fr.inrae.agroclim.indicators.GenerateMarkdown -Dexec.args='${basedir}/src/site/markdown -'
+    - mvn compile exec:java -DskipTests -Dexec.mainClass=fr.inrae.agroclim.indicators.GenerateMarkdown -Dexec.args='${basedir}/src/site/markdown -'
     - mv src/site/markdown/*-en.md src/site/en/markdown/
     - mvn site
     - mv target/site public
-- 
GitLab


From cdbe559ad8284b22c52f9811945530991226f703 Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Thu, 4 Jan 2024 14:33:17 +0100
Subject: [PATCH 13/20] Correction CI pour Pages

---
 .gitlab-ci.yml | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 7806ab01..23a11b0c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -71,11 +71,12 @@ pages_job:
   stage: deploy
   needs: ["package_job"]
   script:
-    - mvn --version
     - mvn compile exec:java -DskipTests -Dexec.mainClass=fr.inrae.agroclim.indicators.GenerateMarkdown -Dexec.args='${basedir}/src/site/markdown -'
     - mv src/site/markdown/*-en.md src/site/en/markdown/
     - mvn site
-    - mv target/site public
   artifacts:
     paths:
-    - public
+      - target/site
+  publish: target/site
+  only:
+    - tags
-- 
GitLab


From 1c16abe87d6aa0eed4700737ac1f389a196a3fec Mon Sep 17 00:00:00 2001
From: Olivier Maury <olivier.maury@inrae.fr>
Date: Thu, 4 Jan 2024 14:36:27 +0100
Subject: [PATCH 14/20] =?UTF-8?q?Mettre=20=C3=A0=20jour=20le=20fichier=20.?=
 =?UTF-8?q?gitlab-ci.yml?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .gitlab-ci.yml | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 23a11b0c..ac6abfca 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -9,7 +9,6 @@ stages:
     - code-check
     - package
     - deploy
-    - pages
 
 cache:
   paths:
@@ -67,7 +66,7 @@ deploy_job:
   script:
     - echo "Maven deploy started"
 
-pages_job:
+pages:
   stage: deploy
   needs: ["package_job"]
   script:
-- 
GitLab


From 4b81cdac97a6ec6a5adbe615d164ca86e987285e Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Thu, 4 Jan 2024 15:33:54 +0100
Subject: [PATCH 15/20] CI : JaCoCo -> Cobertura

---
 .gitlab-ci.yml                                    | 15 ++++++++++++---
 pom.xml                                           |  8 ++++++++
 .../exception/type/CommonErrorType.java           |  2 +-
 3 files changed, 21 insertions(+), 4 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ac6abfca..d73e5273 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -13,12 +13,13 @@ stages:
 cache:
   paths:
     - .m2/repository
+    - target
 
 build_job:
   stage: build
   script:
     - echo "Maven compile started"
-    - mvn compile test-compile
+    - mvn clean compile test-compile
     - ls -lha /usr/bin/tokei
     - /usr/bin/tokei --version
 
@@ -53,6 +54,14 @@ cpd_job:
   script:
     - mvn pmd:cpd
 
+cobertura_job:
+  stage: deploy
+  image: registry.gitlab.com/haynes/jacoco2cobertura:1.0.7
+  needs: ["test_job"]
+  script:
+    # convert report from jacoco to cobertura, using relative project path
+    - python /opt/cover2cover.py target/site/jacoco/jacoco.xml $CI_PROJECT_DIR/src/main/java/ > target/cobertura.xml
+
 package_job:
   stage: package
   script:
@@ -70,9 +79,9 @@ pages:
   stage: deploy
   needs: ["package_job"]
   script:
-    - mvn compile exec:java -DskipTests -Dexec.mainClass=fr.inrae.agroclim.indicators.GenerateMarkdown -Dexec.args='${basedir}/src/site/markdown -'
+    - mvn exec:java -DskipTests -Dexec.mainClass=fr.inrae.agroclim.indicators.GenerateMarkdown -Dexec.args='${basedir}/src/site/markdown -'
     - mv src/site/markdown/*-en.md src/site/en/markdown/
-    - mvn site
+    - mvn site -DskipTests
   artifacts:
     paths:
       - target/site
diff --git a/pom.xml b/pom.xml
index fe9f90b4..4c04a362 100644
--- a/pom.xml
+++ b/pom.xml
@@ -287,6 +287,14 @@ along with Indicators. If not, see <https://www.gnu.org/licenses/>.
                             <goal>report</goal>
                         </goals>
                     </execution>
+                    <execution>
+                        <!-- Generate coverage report XML in target/site/jacoco/ from target/jacoco.exec -->
+                        <id>report-test</id>
+                        <phase>test</phase>
+                        <goals>
+                          <goal>report</goal>
+                        </goals>
+                    </execution>
                     <execution>
                         <!-- Generate coverage report html in target/site/jacoco/ from target/jacoco.exec -->
                         <id>report</id>
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/type/CommonErrorType.java b/src/main/java/fr/inrae/agroclim/indicators/exception/type/CommonErrorType.java
index 8db6e449..5d936c0b 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/exception/type/CommonErrorType.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/type/CommonErrorType.java
@@ -22,7 +22,7 @@ public interface CommonErrorType extends ErrorType {
     CommonErrorType getParent();
 
     /**
-     * @return error name (as {@link Enum#name()).
+     * @return error name (as {@link Enum#name()}).
      */
     String name();
 
-- 
GitLab


From 03e89abbd4c128275a7cba53e7de81231bc2ad72 Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Thu, 4 Jan 2024 15:38:17 +0100
Subject: [PATCH 16/20] CI : JaCoCo -> Cobertura

---
 .gitlab-ci.yml | 21 ++++++++++++++++++++-
 1 file changed, 20 insertions(+), 1 deletion(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index d73e5273..00847c49 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -13,7 +13,6 @@ stages:
 cache:
   paths:
     - .m2/repository
-    - target
 
 build_job:
   stage: build
@@ -22,6 +21,9 @@ build_job:
     - mvn clean compile test-compile
     - ls -lha /usr/bin/tokei
     - /usr/bin/tokei --version
+  artifacts:
+    paths:
+      - target
 
 test_job:
   stage: test
@@ -31,6 +33,8 @@ test_job:
     - mvn test
   artifacts:
     when: always
+    paths:
+      - target
     reports:
       junit:
         - target/surefire-reports/TEST-*.xml
@@ -41,18 +45,27 @@ checkstyle_job:
   needs: ["build_job"]
   script:
     - mvn checkstyle:checkstyle
+  artifacts:
+    paths:
+      - target
 
 pmd_job:
   stage: code-check
   needs: ["build_job"]
   script:
     - mvn pmd:pmd
+  artifacts:
+    paths:
+      - target
 
 cpd_job:
   stage: code-check
   needs: ["build_job"]
   script:
     - mvn pmd:cpd
+  artifacts:
+    paths:
+      - target
 
 cobertura_job:
   stage: deploy
@@ -61,12 +74,18 @@ cobertura_job:
   script:
     # convert report from jacoco to cobertura, using relative project path
     - python /opt/cover2cover.py target/site/jacoco/jacoco.xml $CI_PROJECT_DIR/src/main/java/ > target/cobertura.xml
+  artifacts:
+    paths:
+      - target
 
 package_job:
   stage: package
   script:
     - echo "Maven packaging started"
     - mvn package -DskipTests
+  artifacts:
+    paths:
+      - target
 
 deploy_job:
   stage: deploy
-- 
GitLab


From 3d5363fe151c58a0bad0bced40109c7b2c556b01 Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Thu, 4 Jan 2024 15:52:12 +0100
Subject: [PATCH 17/20] CI : JaCoCo -> Cobertura, ne pas utiliser d'image
 externe

---
 .gitlab-ci.yml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 00847c49..37ac49db 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -69,7 +69,6 @@ cpd_job:
 
 cobertura_job:
   stage: deploy
-  image: registry.gitlab.com/haynes/jacoco2cobertura:1.0.7
   needs: ["test_job"]
   script:
     # convert report from jacoco to cobertura, using relative project path
-- 
GitLab


From 675307d6df53c1f7f8068e2513789f2f9fd100fe Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Thu, 4 Jan 2024 18:30:57 +0100
Subject: [PATCH 18/20] Corrections mineures pour SEASON

---
 pom.xml                                       |  6 ++++
 .../indicators/exception/ErrorMessage.java    | 36 ++++++++-----------
 .../exception/IndicatorsException.java        | 16 ++++-----
 src/main/java/module-info.java                |  1 +
 .../exception/ErrorMessageTest.java           |  6 +++-
 .../exception/IndicatorsExceptionTest.java    | 21 +++++++++++
 6 files changed, 56 insertions(+), 30 deletions(-)
 create mode 100644 src/test/java/fr/inrae/agroclim/indicators/exception/IndicatorsExceptionTest.java

diff --git a/pom.xml b/pom.xml
index 4c04a362..554c8af1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -148,6 +148,12 @@ along with Indicators. If not, see <https://www.gnu.org/licenses/>.
             <artifactId>jackson-dataformat-csv</artifactId>
             <version>2.15.2</version>
         </dependency>
+        <!-- JSON for error output -->
+        <dependency>
+            <groupId>org.json</groupId>
+            <artifactId>json</artifactId>
+            <version>20190722</version>
+        </dependency>
         <!-- JEXL -->
         <dependency>
             <groupId>org.apache.commons</groupId>
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/ErrorMessage.java b/src/main/java/fr/inrae/agroclim/indicators/exception/ErrorMessage.java
index bcf2e83b..674639f3 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/exception/ErrorMessage.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/ErrorMessage.java
@@ -25,6 +25,7 @@ import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
 import lombok.ToString;
+import org.json.JSONObject;
 
 /**
  * Error messages for the user.
@@ -115,28 +116,21 @@ public final class ErrorMessage implements Serializable {
      * @return JSON representation
      */
     public String toJSON() {
-        final StringBuilder sb = new StringBuilder();
-        sb.append("{");
-        sb.append("\"bundleName\": \"").append(bundleName).append("\", ");
-        sb.append("\"errorCode\": \"").append(type.getFullCode()).append("\", ");
-        sb.append("\"errorName\": \"").append(type.getName()).append("\", ");
-        sb.append("\"arguments\": [");
+        final JSONObject json = new JSONObject();
+        json.put("bundleName", bundleName);
+        if (type.getCategory() != null) {
+            final JSONObject cat = new JSONObject();
+            cat.put("code", type.getCategory().getCode());
+            cat.put("name", type.getCategory().getName());
+            json.put("category", cat);
+        }
+        final JSONObject error = new JSONObject();
+        error.put("code", type.getFullCode());
+        error.put("name", type.getName());
+        json.put("error", error);
         if (arguments != null) {
-            boolean first = true;
-            for (final Object arg : arguments) {
-                if (first) {
-                    first = false;
-                } else {
-                    sb.append(", ");
-                }
-                sb.append("\"")
-                .append(arg.toString()
-                        .replace("\"", "\\\"")
-                        .replace("'", "\\'"))
-                .append("\"");
-            }
+            json.put("arguments", arguments.stream().map(Object::toString).toList());
         }
-        sb.append("]}");
-        return sb.toString();
+        return json.toString(2);
     }
 }
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorsException.java b/src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorsException.java
index bef29720..d6f4bf2a 100644
--- a/src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorsException.java
+++ b/src/main/java/fr/inrae/agroclim/indicators/exception/IndicatorsException.java
@@ -6,12 +6,13 @@ import java.util.List;
 /**
  * An exception with {@link ErrorMessage} to describe the error in the Indicators library to the user.
  *
- * Last changed : $Date: 2023-03-16 17:39:08 +0100 (jeu. 16 mars 2023) $
- *
- * @author $Author: omaury $
- * @version $Revision: 1247 $
+ * @author Olivier Maury
  */
-public class IndicatorsException  extends ErrorMessageException {
+public class IndicatorsException extends ErrorMessageException {
+    /**
+     * Path of .property resource.
+     */
+    private static final String BUNDLE_NAME = "fr.inrae.agroclim.indicators.resources.messages";
 
     /**
      * UUID for Serializable.
@@ -25,7 +26,7 @@ public class IndicatorsException  extends ErrorMessageException {
      * @param arguments Arguments for the message.
      */
     public IndicatorsException(final ErrorType errorType, final Serializable... arguments) {
-        super(new ErrorMessage("fr.inrae.agroclim.indicators.resources.messages", errorType, List.of(arguments)));
+        super(new ErrorMessage(BUNDLE_NAME, errorType, List.of(arguments)));
     }
 
     /**
@@ -39,7 +40,6 @@ public class IndicatorsException  extends ErrorMessageException {
      *         unknown.)
      */
     public IndicatorsException(final ErrorType errorType, final Throwable cause, final Serializable... arguments) {
-        super(new ErrorMessage("fr.inrae.agroclim.indicators.resources.messages", errorType, List.of(arguments)),
-                cause);
+        super(new ErrorMessage(BUNDLE_NAME, errorType, List.of(arguments)), cause);
     }
 }
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 88e38656..d3f3036b 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -30,6 +30,7 @@ module fr.inrae.agroclim.indicators {
     requires transitive java.desktop;
     requires org.apache.logging.log4j;
     requires org.apache.logging.log4j.core;
+    requires org.json;
 
     exports fr.inrae.agroclim.indicators.exception;
     exports fr.inrae.agroclim.indicators.model;
diff --git a/src/test/java/fr/inrae/agroclim/indicators/exception/ErrorMessageTest.java b/src/test/java/fr/inrae/agroclim/indicators/exception/ErrorMessageTest.java
index 9d8cbcaa..a6324951 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/exception/ErrorMessageTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/exception/ErrorMessageTest.java
@@ -17,6 +17,8 @@
 package fr.inrae.agroclim.indicators.exception;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 
 import java.io.Serializable;
 import java.util.Arrays;
@@ -95,7 +97,6 @@ public class ErrorMessageTest {
      */
     @Test
     public void getMessageWithArguments() {
-
         final ErrorI18nKey key = ErrorI18nKey.KEY;
         final String category = "climate";
         final Integer line = 11;
@@ -109,6 +110,9 @@ public class ErrorMessageTest {
         expected = defaultResources.format(key.getI18nKey(), category, line);
         found = error.getMessage();
         assertEquals(expected, found);
+
+        assertNotNull(error.toJSON());
+        assertFalse(error.toJSON().isBlank());
     }
 
     /**
diff --git a/src/test/java/fr/inrae/agroclim/indicators/exception/IndicatorsExceptionTest.java b/src/test/java/fr/inrae/agroclim/indicators/exception/IndicatorsExceptionTest.java
new file mode 100644
index 00000000..72fdf172
--- /dev/null
+++ b/src/test/java/fr/inrae/agroclim/indicators/exception/IndicatorsExceptionTest.java
@@ -0,0 +1,21 @@
+package fr.inrae.agroclim.indicators.exception;
+
+import fr.inrae.agroclim.indicators.exception.type.ComputationErrorType;
+import static org.junit.Assert.assertEquals;
+import org.junit.Test;
+
+/**
+ * Test translations and formatting of exception message.
+ *
+ * @author Olivier Maury
+ */
+public class IndicatorsExceptionTest {
+
+    @Test
+    public void getLocalizedMessage() {
+        var instance = new IndicatorsException(ComputationErrorType.FORMULA, "m * c * c");
+        var actual = instance.getLocalizedMessage();
+        var expected = "Error while executing expression \"m * c * c\".";
+        assertEquals(expected, actual);
+    }
+}
-- 
GitLab


From 06a41f4011d2b8b40f45eb37994ecb0f02542bff Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Fri, 5 Jan 2024 08:06:46 +0100
Subject: [PATCH 19/20] Fixer la locale pour le test.

---
 .../agroclim/indicators/exception/IndicatorsExceptionTest.java  | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/test/java/fr/inrae/agroclim/indicators/exception/IndicatorsExceptionTest.java b/src/test/java/fr/inrae/agroclim/indicators/exception/IndicatorsExceptionTest.java
index 72fdf172..63988bc0 100644
--- a/src/test/java/fr/inrae/agroclim/indicators/exception/IndicatorsExceptionTest.java
+++ b/src/test/java/fr/inrae/agroclim/indicators/exception/IndicatorsExceptionTest.java
@@ -1,6 +1,7 @@
 package fr.inrae.agroclim.indicators.exception;
 
 import fr.inrae.agroclim.indicators.exception.type.ComputationErrorType;
+import java.util.Locale;
 import static org.junit.Assert.assertEquals;
 import org.junit.Test;
 
@@ -13,6 +14,7 @@ public class IndicatorsExceptionTest {
 
     @Test
     public void getLocalizedMessage() {
+        Locale.setDefault(Locale.ENGLISH);
         var instance = new IndicatorsException(ComputationErrorType.FORMULA, "m * c * c");
         var actual = instance.getLocalizedMessage();
         var expected = "Error while executing expression \"m * c * c\".";
-- 
GitLab


From c12056d99fa85cd5976ae06f7862e3419913efa1 Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Fri, 5 Jan 2024 10:02:11 +0100
Subject: [PATCH 20/20] =?UTF-8?q?Suppression=20d'exceptions=20non=20utilis?=
 =?UTF-8?q?=C3=A9es?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 pom.xml                                       |  2 +-
 .../exception/AbstractException.java          | 59 -------------------
 .../exception/FunctionalException.java        | 48 ---------------
 .../exception/TechnicalException.java         | 46 ---------------
 4 files changed, 1 insertion(+), 154 deletions(-)
 delete mode 100644 src/main/java/fr/inrae/agroclim/indicators/exception/AbstractException.java
 delete mode 100644 src/main/java/fr/inrae/agroclim/indicators/exception/FunctionalException.java
 delete mode 100644 src/main/java/fr/inrae/agroclim/indicators/exception/TechnicalException.java

diff --git a/pom.xml b/pom.xml
index 554c8af1..e8fcf487 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,7 +22,7 @@ along with Indicators. If not, see <https://www.gnu.org/licenses/>.
     <name>Indicators</name>
     <description>Library of agro- and eco-climatic indicators.</description>
     <inceptionYear>2018</inceptionYear>
-    <version>1.3.0-SNAPSHOT</version>
+    <version>2.0.0-SNAPSHOT</version>
     <packaging>jar</packaging>
     <licenses>
         <license>
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/AbstractException.java b/src/main/java/fr/inrae/agroclim/indicators/exception/AbstractException.java
deleted file mode 100644
index 86b51eb9..00000000
--- a/src/main/java/fr/inrae/agroclim/indicators/exception/AbstractException.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * 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 <https://www.gnu.org/licenses/>.
- */
-package fr.inrae.agroclim.indicators.exception;
-
-/**
- * Base class for FunctionalException and TechnicalException.
- */
-public abstract class AbstractException extends Exception {
-    /**
-     * UUID for Serializable.
-     */
-    private static final long serialVersionUID = 6030595237342400001L;
-
-    /**
-     * Root exception.
-     */
-    private final Exception rootException;
-
-    /**
-     * @param msg
-     *            exception message
-     */
-    protected AbstractException(final String msg) {
-        super(msg);
-        rootException = null;
-    }
-
-    /**
-     * @param msg
-     *            exception message
-     * @param e
-     *            root exception
-     */
-    protected AbstractException(final String msg, final Exception e) {
-        super(msg);
-        this.rootException = e;
-    }
-
-    /**
-     * @return Root exception.
-     */
-    public final Exception getRootException() {
-        return rootException;
-    }
-}
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/FunctionalException.java b/src/main/java/fr/inrae/agroclim/indicators/exception/FunctionalException.java
deleted file mode 100644
index 831c2c77..00000000
--- a/src/main/java/fr/inrae/agroclim/indicators/exception/FunctionalException.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * 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 <https://www.gnu.org/licenses/>.
- */
-package fr.inrae.agroclim.indicators.exception;
-
-/**
- * For Indicator, Evaluation and AggregationFunction.
- *
- * @deprecated Use {@link IndicatorsException}.
- */
-@Deprecated
-public class FunctionalException extends AbstractException {
-    /**
-     * UUID for Serializable.
-     */
-    private static final long serialVersionUID = 6030595237342400002L;
-
-    /**
-     * @param msg
-     *            exception message
-     */
-    public FunctionalException(final String msg) {
-        super(msg);
-    }
-
-    /**
-     * @param msg
-     *            exception message
-     * @param e
-     *            root exception
-     */
-    public FunctionalException(final String msg, final Exception e) {
-        super(msg, e);
-    }
-}
diff --git a/src/main/java/fr/inrae/agroclim/indicators/exception/TechnicalException.java b/src/main/java/fr/inrae/agroclim/indicators/exception/TechnicalException.java
deleted file mode 100644
index ecee955e..00000000
--- a/src/main/java/fr/inrae/agroclim/indicators/exception/TechnicalException.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * 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 <https://www.gnu.org/licenses/>.
- */
-package fr.inrae.agroclim.indicators.exception;
-
-/**
- * For XMLUtils.
- *
- * @deprecated Use {@link IndicatorsException}.
- */
-@Deprecated
-public class TechnicalException extends AbstractException {
-
-    /**
-     * UUID for Serializable.
-     */
-    private static final long serialVersionUID = 6030595237342400003L;
-
-    /**
-     * @param msg exception message
-     */
-    public TechnicalException(final String msg) {
-        super(msg);
-    }
-
-    /**
-     * @param msg exception message
-     * @param e root exception
-     */
-    public TechnicalException(final String msg, final Exception e) {
-        super(msg, e);
-    }
-}
-- 
GitLab