GenerateMarkdown.java

package fr.inrae.agroclim.indicators;

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 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 lombok.extern.log4j.Log4j2;

/**
 * Utility to create Markdown and CSV files for Hugo and Maven site.
 *
 * @author Olivier Maury
 */
@Log4j2
public class GenerateMarkdown {

    /**
     * Markdown YAML front matter.
     */
    private static final String FRONT_MATTER = """
                                       ---
                                       title: "%s"
                                       description: "%s"
                                       keywords: "%s"
                                       date: %s
                                       ---

                                       """;

    /**
     * @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));
        // The directory where Markdown files are generated.
        final String outDir;
        // Separator between file base name and language code.
        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 = ".";
        }

        for (final Locale locale : Arrays.asList(Locale.ENGLISH, Locale.FRENCH)) {
            final String languageCode = locale.getLanguage();
            Path path = Paths.get(outDir, "errors" + languageSep + languageCode + ".md");
            var instance = new GenerateMarkdown(locale, null);
            instance.writeErrorMdFile(path);
            for (final TimeScale timescale : TimeScale.values()) {
                LOGGER.trace("Generating files for {}...", timescale);
                LOGGER.trace("Generating files for {}...", timescale);
                instance = new GenerateMarkdown(locale, timescale);
                final String suffix = "-" + timescale.name().toLowerCase();
                path = Paths.get(outDir, "indicators" + suffix + languageSep + languageCode + ".md");
                instance.writeIndicatorsMdFiles(path);
                path = Paths.get(outDir, "indicators" + suffix + ".csv");
                instance.writeIndicatorsCsvFiles(path);
                path = Paths.get(outDir, "parameters" + suffix + ".csv");
                instance.writeParametersCsvFiles(path);
                LOGGER.trace("Generating files for {}... done", timescale);
            }
        }
    }

    /**
     * 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");
    }
    /**
     * Creation date for front matter.
     */
    private final String created;

    /**
     * I18n messages.
     */
    private final I18n i18n;
    /**
     * Knowledge used.
     */
    private final Knowledge knowledge;
    /**
     * Locale used to write files.
     */
    private final Locale locale;

    /**
     * Constructor.
     *
     * @param l locale used to generate the file. Not all files are localized.
     * @param timescale timescale of knowledge
     * @throws IndicatorsException error while loading knowledge
     */
    public GenerateMarkdown(final Locale l, final TimeScale timescale) throws IndicatorsException {
        final DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
        created = df.format(new Date());
        final String bundleName = "fr.inrae.agroclim.indicators.resources.messages";
        i18n = new I18n(bundleName, l);
        this.locale = l;
        if (timescale == null) {
            this.knowledge = null;
        } else {
            this.knowledge = Knowledge.load(timescale);
        }
    }

    /**
     * Write the Markdown file showing all error codes and descriptions.
     *
     * @param path output file path
     * @throws IOException file not found or error while writing
     */
    public void writeErrorMdFile(final Path path) throws IOException {
        LOGGER.trace(path);
        final String indicatorsVersion = fr.inrae.agroclim.indicators.resources.Version.getString("version");
        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));
            writer.write(i18n.format("markdown.error.version", indicatorsVersion) + "\n\n");
            writer.write(i18n.get("markdown.error.feedback") + "\n\n");
            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);
            }
        }
    }

    /**
     * Whatever is the locale, the file is the same.
     *
     * @param path output file path
     * @throws IOException
     */
    public void writeIndicatorsCsvFiles(final Path path) throws IOException {
        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'indicateur
                    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 path output file path
     * @throws IOException
     */
    public void writeIndicatorsMdFiles(final Path path) throws IOException {
        final String languageCode = locale.getLanguage();
        final TimeScale timescale = knowledge.getTimescale();

        LOGGER.trace(path);
        try (BufferedWriter mdWriter = new Utf8BufferedWriter(path)) {
            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'indicateur
                    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"));
        }
    }

    /**
     * Whatever is the locale, the file is the same.
     *
     * @param path output file path
     * @throws IOException
     */
    public void writeParametersCsvFiles(final Path path) throws IOException {
        LOGGER.trace(path);
        try (BufferedWriter paramWriter = new Utf8BufferedWriter(path)) {
            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");
            }
        }
    }
}