Knowledge.java

/*
 * 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.model;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;

import fr.inrae.agroclim.indicators.exception.IndicatorsException;
import fr.inrae.agroclim.indicators.model.criteria.ComparisonCriteria;
import fr.inrae.agroclim.indicators.model.criteria.CompositeCriteria;
import fr.inrae.agroclim.indicators.model.criteria.FormulaCriteria;
import fr.inrae.agroclim.indicators.model.criteria.LogicalOperator;
import fr.inrae.agroclim.indicators.model.criteria.NoCriteria;
import fr.inrae.agroclim.indicators.model.criteria.RelationalOperator;
import fr.inrae.agroclim.indicators.model.criteria.SimpleCriteria;
import fr.inrae.agroclim.indicators.model.function.aggregation.AggregationFunction;
import fr.inrae.agroclim.indicators.model.function.aggregation.JEXLFunction;
import fr.inrae.agroclim.indicators.model.function.normalization.Exponential;
import fr.inrae.agroclim.indicators.model.function.normalization.Linear;
import fr.inrae.agroclim.indicators.model.function.normalization.Normal;
import fr.inrae.agroclim.indicators.model.function.normalization.NormalizationFunction;
import fr.inrae.agroclim.indicators.model.function.normalization.Sigmoid;
import fr.inrae.agroclim.indicators.model.indicator.Average;
import fr.inrae.agroclim.indicators.model.indicator.AverageOfDiff;
import fr.inrae.agroclim.indicators.model.indicator.CompositeIndicator;
import fr.inrae.agroclim.indicators.model.indicator.DayOfYear;
import fr.inrae.agroclim.indicators.model.indicator.DiffOfSum;
import fr.inrae.agroclim.indicators.model.indicator.Formula;
import fr.inrae.agroclim.indicators.model.indicator.Indicator;
import fr.inrae.agroclim.indicators.model.indicator.IndicatorCategory;
import fr.inrae.agroclim.indicators.model.indicator.InjectedParameter;
import fr.inrae.agroclim.indicators.model.indicator.Max;
import fr.inrae.agroclim.indicators.model.indicator.MaxWaveLength;
import fr.inrae.agroclim.indicators.model.indicator.Min;
import fr.inrae.agroclim.indicators.model.indicator.NumberOfDays;
import fr.inrae.agroclim.indicators.model.indicator.NumberOfWaves;
import fr.inrae.agroclim.indicators.model.indicator.PotentialSowingDaysFrequency;
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.xml.XMLUtil;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlAttribute;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlElementWrapper;
import jakarta.xml.bind.annotation.XmlRootElement;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.log4j.Log4j2;

/**
 * Object loaded from knowledge.xml.
 *
 * @author Olivier Maury
 */
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@Log4j2
@ToString
public final class Knowledge implements Cloneable {
    /**
     * The classes needed to load XML knowledge file.
     */
    public static final Class<?>[] CLASSES_FOR_JAXB = {Knowledge.class,
            LocalizedString.class, Parameter.class, TimeScale.class, Note.class,
            /*- Indicators */
            Average.class, AverageOfDiff.class, CompositeIndicator.class, DayOfYear.class, DiffOfSum.class,
            Formula.class, Indicator.class, InjectedParameter.class, Max.class, MaxWaveLength.class, Min.class,
            Normal.class, NumberOfDays.class, NumberOfWaves.class,
            PotentialSowingDaysFrequency.class, Quotient.class,
            SimpleIndicator.class, Sum.class, Tamm.class,
            /*- Normalization */
            AggregationFunction.class, Exponential.class, JEXLFunction.class,
            Linear.class, NormalizationFunction.class, Sigmoid.class,
            /*- Criteria */
            ComparisonCriteria.class, CompositeCriteria.class, FormulaCriteria.class,
            LogicalOperator.class, NoCriteria.class, RelationalOperator.class,
            SimpleCriteria.class};

    /**
     * URL of knowledge.xml.
     */
    public static final Map<TimeScale, String> RESOURCES;

    static {
        RESOURCES = new EnumMap<>(TimeScale.class);
        RESOURCES.put(TimeScale.DAILY, "/fr/inrae/agroclim/indicators/knowledge.xml");
        RESOURCES.put(TimeScale.HOURLY, "/fr/inrae/agroclim/indicators/knowledge_hourly.xml");
    }

    /**
     * Loop on indicator list to get the Indicator defined its id.
     *
     * @param indicatorId indicator id
     * @param indicators indicator list
     * @return indicator matching id or null
     */
    private static Indicator getIndicator(@NonNull final String indicatorId,
            final List<? extends Indicator> indicators) {
        Indicator result = null;
        for (final Indicator i : indicators) {
            if (indicatorId.equals(i.getId())) {
                result = i;
                break;
            }
        }
        return result;
    }

    /**
     * Deserialize Knowledge from embedded file for daily indicators.
     *
     * @return deserialized Knowledge
     * @throws IndicatorsException error while loading knowledge
     */
    public static Knowledge load() throws IndicatorsException {
        return load(TimeScale.DAILY);
    }

    /**
     * Deserialize Knowledge from input stream.
     *
     * @param stream
     *            input stream
     * @return deserialized Knowledge
     * @throws IndicatorsException
     *             error while loading knowledge
     */
    private static Knowledge load(final InputStream stream) throws IndicatorsException {
        try {
            final Knowledge knowledge = (Knowledge) XMLUtil.loadResource(stream,
                    CLASSES_FOR_JAXB);
            knowledge.ecophysiologicalProcesses.forEach(ind ->
            ind.setIndicatorCategory(IndicatorCategory.ECOPHYSIOLOGICAL_PROCESSES));
            knowledge.indicators.forEach(ind ->
            ind.setIndicatorCategory(IndicatorCategory.CLIMATIC_EFFECTS));
            knowledge.culturalPractices.forEach(ind ->
            ind.setIndicatorCategory(IndicatorCategory.CULTURAL_PRACTICES));
            return knowledge;
        } catch (final IndicatorsException ex) {
            LOGGER.error(ex);
            throw ex;
        }
    }

    /**
     * Deserialize Knowledge from embedded file.
     *
     * @param timescale timescale of indicators
     * @return deserialized Knowledge
     * @throws IndicatorsException error while loading knowledge
     */
    public static Knowledge load(final TimeScale timescale) throws IndicatorsException {
        final InputStream stream = Knowledge.class.getResourceAsStream(RESOURCES.get(timescale));
        return load(stream);
    }

    /**
     * Indicators for cultural practices.
     */
    @XmlElementWrapper(name = "culturalPractices")
    @XmlElement(name = "culturalPractice")
    @Setter
    private List<CompositeIndicator> culturalPractices;

    /**
     * Indicators for ecophysiological processes.
     */
    @XmlElementWrapper(name = "ecophysiologicalProcesses")
    @XmlElement(name = "ecophysiologicalProcess")
    @Setter
    private List<CompositeIndicator> ecophysiologicalProcesses;

    /**
     * Parameters for indicator and indicator criteria.
     */
    @XmlElementWrapper(name = "parameters")
    @XmlElement(name = "parameter")
    @Getter
    @Setter
    private List<Parameter> parameters;

    /**
     * Timescale of indicators.
     */
    @Getter
    @Setter
    @XmlAttribute
    private TimeScale timescale = TimeScale.DAILY;

    /**
     * Notes library for indicators.
     */
    @XmlElementWrapper(name = "notes")
    @XmlElement(name = "note")
    @Getter
    @Setter
    private List<Note> notes;

    /**
     * Measurement units for indicators.
     */
    @XmlElementWrapper(name = "units")
    @XmlElement(name = "unit")
    @Getter
    @Setter
    private List<Unit> units;

    /**
     * Indicators for climatic effets.
     */
    @XmlElementWrapper(name = "climaticEffects")
    @XmlElement(name = "climaticEffect")
    @Getter
    @Setter
    private List<CompositeIndicator> indicators;

    /**
     * Constructor.
     */
    public Knowledge() {
        culturalPractices = new ArrayList<>();
        ecophysiologicalProcesses = new ArrayList<>();
        indicators = new ArrayList<>();
    }

    @Override
    public Knowledge clone() {
        final Knowledge clone = new Knowledge();
        culturalPractices.forEach(practice -> clone.culturalPractices.add(practice.clone()));
        ecophysiologicalProcesses.forEach(process -> clone.ecophysiologicalProcesses.add(process.clone()));
        indicators.forEach(clone.indicators::add);
        return clone;
    }

    /**
     * @param indicatorId indicator id
     * @return indicator in climatic effects category
     */
    private Indicator getClimaticEffectsIndicator(final String indicatorId) {
        return getIndicator(indicatorId, indicators);
    }

    /**
     * @return indicators in cultural practices category
     */
    public List<CompositeIndicator> getCulturalPractices() {
        return culturalPractices;
    }

    /**
     * @return indicators in ecophysiological processes category
     */
    public List<CompositeIndicator> getEcophysiologicalProcesses() {
        return ecophysiologicalProcesses;
    }

    /**
     * Loop on indicator in knowledge to get the Indicator defined its id.
     *
     * @param indicatorId indicator id
     * @return indicator matching id or null
     */
    public Indicator getIndicator(@NonNull final String indicatorId) {
        Indicator result;
        for (final CompositeIndicator indicator : indicators) {
            if (indicatorId.equals(indicator.getId())) {
                return indicator;
            }
            result = getIndicator(indicatorId, indicator.getIndicators());
            if (result != null) {
                result.setParent(indicator);
                return result;
            }
        }
        for (final CompositeIndicator indicator : culturalPractices) {
            if (indicatorId.equals(indicator.getId())) {
                return indicator;
            }
            result = getIndicator(indicatorId, indicator.getIndicators());
            if (result != null) {
                result.setParent(indicator);
                return result;
            }
        }
        for (final CompositeIndicator indicator : ecophysiologicalProcesses) {
            if (indicatorId.equals(indicator.getId())) {
                return indicator;
            }
            result = getIndicator(indicatorId, indicator.getIndicators());
            if (result != null) {
                result.setParent(indicator);
                return result;
            }
        }
        return null;
    }

    /**
     * Get child indicators for the CLIMATIC indicator or the category.
     *
     * @param ind indicator with sub indicators
     * @return child indicators for the category
     */
    public List<? extends Indicator> getNextIndicators(
            final CompositeIndicator ind) {
        final IndicatorCategory category = ind.getIndicatorCategory();
        final String id = ind.getId();
        if (category == null) {
            throw new IllegalArgumentException("No indicator category for "
                    + id);
        }
        switch (category) {
        case PHENO_PHASES:
            return getEcophysiologicalProcesses();
        case CULTURAL_PRACTICES:
            if (ind.isPhase()) {
                return getCulturalPractices();
            }
            return getIndicators();
        case ECOPHYSIOLOGICAL_PROCESSES:
            if (ind.isPhase()) {
                return getEcophysiologicalProcesses();
            }
            return getIndicators();
        case CLIMATIC_EFFECTS:
            return ((CompositeIndicator) getClimaticEffectsIndicator(id))
                    .getIndicators();
        default:
            LOGGER.fatal("Not handled category: " + category);
            throw new IllegalArgumentException("Not handled category: "
                    + category);
        }
    }

    /**
     * Get the first parameter matching id.
     *
     * @param id parameter id
     * @return parameter or null if not found
     */
    public Parameter getParameterById(final String id) {
        return this.parameters.stream()
                .filter(parameter -> parameter.getId().equals(id))
                .findFirst().orElse(null);
    }

    /**
     * Update localized description and name of indicator from Knowledge.
     *
     * @param indicator indicator to update
     */
    public void setI18n(final Indicator indicator) {
        if (indicator.getId() == null) {
            throw new IllegalArgumentException("indicator.id must not be null! "
                    + indicator.getId());
        }
        final Indicator ind = getIndicator(indicator.getId());
        if (ind != null) {
            if (indicator.getNames() == null) {
                indicator.setNames(new ArrayList<>());
            } else {
                indicator.getNames().clear();
            }
            if (ind.getNames() != null) {
                indicator.getNames().addAll(ind.getNames());
            }
            if (indicator.getDescriptions() == null) {
                indicator.setDescriptions(new ArrayList<>());
            } else {
                indicator.getDescriptions().clear();
            }
            if (ind.getDescriptions() != null) {
                indicator.getDescriptions().addAll(ind.getDescriptions());
            }
        }
        if (indicator instanceof CompositeIndicator) {
            ((CompositeIndicator) indicator).getIndicators().forEach(this::setI18n);
        }
    }
}