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");
}
}
}
}