View Javadoc
1   package fr.inrae.agroclim.indicators;
2   
3   import java.io.BufferedWriter;
4   import java.io.IOException;
5   import java.nio.file.Path;
6   import java.nio.file.Paths;
7   import java.text.DateFormat;
8   import java.text.SimpleDateFormat;
9   import java.util.ArrayList;
10  import java.util.Arrays;
11  import java.util.Collections;
12  import java.util.Date;
13  import java.util.HashSet;
14  import java.util.LinkedList;
15  import java.util.List;
16  import java.util.Locale;
17  import java.util.Set;
18  
19  import fr.inrae.agroclim.indicators.exception.IndicatorsException;
20  import fr.inrae.agroclim.indicators.exception.type.CommonErrorType;
21  import fr.inrae.agroclim.indicators.exception.type.ComputationErrorType;
22  import fr.inrae.agroclim.indicators.exception.type.ResourceErrorType;
23  import fr.inrae.agroclim.indicators.exception.type.XmlErrorType;
24  import fr.inrae.agroclim.indicators.model.Knowledge;
25  import fr.inrae.agroclim.indicators.model.LocalizedString;
26  import fr.inrae.agroclim.indicators.model.Note;
27  import fr.inrae.agroclim.indicators.model.Parameter;
28  import fr.inrae.agroclim.indicators.model.TimeScale;
29  import fr.inrae.agroclim.indicators.model.indicator.CompositeIndicator;
30  import fr.inrae.agroclim.indicators.model.indicator.Indicator;
31  import fr.inrae.agroclim.indicators.resources.I18n;
32  import fr.inrae.agroclim.indicators.util.StringUtils;
33  import fr.inrae.agroclim.indicators.util.Utf8BufferedWriter;
34  import lombok.extern.log4j.Log4j2;
35  
36  /**
37   * Utility to create Markdown and CSV files for Hugo and Maven site.
38   *
39   * @author Olivier Maury
40   */
41  @Log4j2
42  public class GenerateMarkdown {
43  
44      /**
45       * Mardown YAML front matter.
46       */
47      private static final String FRONT_MATTER = """
48                                         ---
49                                         title: "%s"
50                                         description: "%s"
51                                         keywords: "%s"
52                                         date: %s
53                                         ---
54  
55                                         """;
56  
57      /**
58       * @param args arguments : outDir, languageSep
59       * @throws IndicatorsException while loading knowledge
60       * @throws java.io.IOException while writing file
61       */
62      public static void main(final String[] args) throws IndicatorsException, IOException {
63          LOGGER.traceEntry("Arguments: {}", Arrays.asList(args));
64          // The directory where Markdown files are generated.
65          final String outDir;
66          // Separator between file base name and language code.
67          final String languageSep;
68          if (args != null && args.length > 0) {
69              outDir = args[0];
70              if (args.length > 1) {
71                  languageSep = args[1];
72              } else {
73                  languageSep = ".";
74              }
75          } else {
76              outDir = System.getProperty("java.io.tmpdir");
77              languageSep = ".";
78          }
79  
80          for (final Locale locale : Arrays.asList(Locale.ENGLISH, Locale.FRENCH)) {
81              final String languageCode = locale.getLanguage();
82              Path path = Paths.get(outDir, "errors" + languageSep + languageCode + ".md");
83              var instance = new GenerateMarkdown(locale, null);
84              instance.writeErrorMdFile(path);
85              for (final TimeScale timescale : TimeScale.values()) {
86                  LOGGER.trace("Generating files for {}...", timescale);
87                  LOGGER.trace("Generating files for {}...", timescale);
88                  instance = new GenerateMarkdown(locale, timescale);
89                  final String suffix = "-" + timescale.name().toLowerCase();
90                  path = Paths.get(outDir, "indicators" + suffix + languageSep + languageCode + ".md");
91                  instance.writeIndicatorsMdFiles(path);
92                  path = Paths.get(outDir, "indicators" + suffix + ".csv");
93                  instance.writeIndicatorsCsvFiles(path);
94                  path = Paths.get(outDir, "parameters" + suffix + ".csv");
95                  instance.writeParametersCsvFiles(path);
96                  LOGGER.trace("Generating files for {}... done", timescale);
97              }
98          }
99      }
100 
101     /**
102      * Write a line in a table.
103      *
104      * @param writer the writer to user
105      * @param values strings to write
106      * @throws IOException when using BufferedWriter.write
107      */
108     private static void writeLn(final BufferedWriter writer,
109             final String... values) throws IOException {
110         final int nb = values.length;
111         writer.write("| ");
112         for (int i = 0; i < nb; i++) {
113             writer.write(values[i]);
114             if (i < nb - 1) {
115                 writer.write(" | ");
116             }
117         }
118         writer.write(" |\n");
119     }
120     /**
121      * Creation date for front matter.
122      */
123     private final String created;
124 
125     /**
126      * I18n messages.
127      */
128     private final I18n i18n;
129     /**
130      * Knowledge used.
131      */
132     private final Knowledge knowledge;
133     /**
134      * Locale used to write files.
135      */
136     private final Locale locale;
137 
138     /**
139      * Constructor.
140      *
141      * @param l locale used to generate the file. Not all files are localized.
142      * @param timescale timescale of knowledge
143      * @throws IndicatorsException error while loading knowledge
144      */
145     public GenerateMarkdown(final Locale l, final TimeScale timescale) throws IndicatorsException {
146         final DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
147         created = df.format(new Date());
148         final String bundleName = "fr.inrae.agroclim.indicators.resources.messages";
149         i18n = new I18n(bundleName, l);
150         this.locale = l;
151         if (timescale == null) {
152             this.knowledge = null;
153         } else {
154             this.knowledge = Knowledge.load(timescale);
155         }
156     }
157 
158     /**
159      * Write the Markdown file showing all error codes and descriptions.
160      *
161      * @param path output file path
162      * @throws IOException file not found or error while writing
163      */
164     public void writeErrorMdFile(final Path path) throws IOException {
165         LOGGER.trace(path);
166         final String indicatorsVersion = fr.inrae.agroclim.indicators.resources.Version.getString("version");
167         try (BufferedWriter writer = new Utf8BufferedWriter(path)) {
168             writer.write(String.format(FRONT_MATTER,
169                     i18n.get("markdown.error.title"),
170                     i18n.get("markdown.error.description"),
171                     i18n.get("markdown.error.keywords"),
172                     created));
173             writer.write(i18n.format("markdown.error.version", indicatorsVersion) + "\n\n");
174             writer.write(i18n.get("markdown.error.feedback") + "\n\n");
175             writeLn(writer, i18n.get("markdown.error.fullcode"), i18n.get("markdown.error.name"),
176                     i18n.get("markdown.error.message"));
177             writer.write("|:----------|:-----------|:-----------|\n");
178             final List<CommonErrorType> types = new ArrayList<>();
179             types.addAll(Arrays.asList(XmlErrorType.values()));
180             types.addAll(Arrays.asList(ResourceErrorType.values()));
181             types.addAll(Arrays.asList(ComputationErrorType.values()));
182             String previousCat = "";
183             for (final CommonErrorType type : types) {
184                 var cat = type.getCategory().getCategory(i18n);
185                 if (!previousCat.equals(cat)) {
186                     var catCode = type.getCategory().getCode();
187                     writeLn(writer, "**" + i18n.get("markdown.error.category") + " `" + catCode + "` - " + cat + "**");
188                     previousCat = cat;
189                 }
190                 var fullCode = type.getFullCode();
191                 var name = type.getName();
192                 var description = i18n.get(type.getI18nKey());
193                 writeLn(writer, fullCode, name, description);
194             }
195         }
196     }
197 
198     /**
199      * Whatever is the locale, the file is the same.
200      *
201      * @param path output file path
202      * @throws IOException
203      */
204     public void writeIndicatorsCsvFiles(final Path path) throws IOException {
205         LOGGER.trace(path);
206         try (BufferedWriter writer = new Utf8BufferedWriter(path)) {
207             writer.write("id;nom_en;nom_fr;description_en;description_fr;variables;param\u00e8tres;notes\n");
208             for (final CompositeIndicator comp : knowledge.getIndicators()) {
209                 for (final Indicator ind : comp.getIndicators()) {
210                     writer.write(ind.getId());
211                     writer.write(";");
212                     writer.write(ind.getName("en"));
213                     writer.write(";");
214                     if (!ind.getName("fr").equals(ind.getName("en"))) {
215                         writer.write(ind.getName("fr"));
216                     }
217                     writer.write(";");
218                     final String description = ind.getDescription("fr");
219                     if (!description.equals(ind.getDescription("en"))) {
220                         writer.write(ind.getDescription("en"));
221                     }
222                     writer.write(";");
223                     writer.write(description);
224                     writer.write(";");
225                     final List<String> variables = new LinkedList<>();
226                     if (ind.getVariables() != null && !ind.getVariables().isEmpty()) {
227                         ind.getVariables().forEach(variable -> variables.add(variable.getName()));
228                         Collections.sort(variables);
229                         writer.write(String.join(", ", variables));
230                     }
231                     writer.write(";");
232                     final List<String> parameters = new LinkedList<>();
233                     if (ind.getParameters() != null
234                             && !ind.getParameters().isEmpty()) {
235                         ind.getParameters().forEach(param -> parameters.add(param.getId()));
236                         Collections.sort(parameters);
237                         writer.write(String.join(", ", parameters));
238                     }
239                     // affichage des références des notes de l'indicateurs
240                     writer.write(";");
241                     final List<String> notes = new LinkedList<>();
242                     if (ind.getNotes() != null && !ind.getNotes().isEmpty()) {
243                         ind.getNotes().forEach(note -> notes.add(note.getId()));
244                         writer.write(String.join(", ", notes));
245                     }
246                     writer.write("\n");
247                 }
248             }
249         }
250     }
251 
252     /**
253      * @param path output file path
254      * @throws IOException
255      */
256     public void writeIndicatorsMdFiles(final Path path) throws IOException {
257         final String languageCode = locale.getLanguage();
258         final TimeScale timescale = knowledge.getTimescale();
259 
260         LOGGER.trace(path);
261         try (BufferedWriter mdWriter = new Utf8BufferedWriter(path)) {
262             final long nb = knowledge.getIndicators().stream().mapToInt(comp -> comp.getIndicators().size()).sum();
263             final String indicatorsVersion = fr.inrae.agroclim.indicators.resources.Version.getString("version");
264             final String frontMatter = """
265                                        ---
266                                        title: %s
267                                        description: %s
268                                        keywords: %s
269                                        date: %s
270                                        ---
271 
272                                        """;
273             mdWriter.write(String.format(frontMatter,
274                     i18n.get("markdown.title." + timescale.name().toLowerCase()),
275                     i18n.get("markdown.description." + timescale.name().toLowerCase()),
276                     i18n.get("markdown.keywords"),
277                     created));
278             mdWriter.write(i18n.format("markdown.indicators.version", indicatorsVersion) + "\n\n"
279                     + "## " + i18n.format("markdown.indicators." + timescale.name().toLowerCase(), nb) + "\n");
280             writeLn(mdWriter, i18n.get("markdown.id"), i18n.get("markdown.name"), i18n.get("markdown.description"),
281                     i18n.get("markdown.variables"), i18n.get("markdown.parameters"),
282                     i18n.get("markdown.unit") + " [^1]", i18n.get("markdown.notes"));
283             mdWriter.write("|:---|:-----|:------------|:----------|:-----------|:-----------|:-----------|\n");
284 
285             final Set<String> allVariables = new HashSet<>();
286             for (final CompositeIndicator comp : knowledge.getIndicators()) {
287                 writeLn(mdWriter, "**" + comp.getName(languageCode) + "**");
288                 for (final Indicator ind : comp.getIndicators()) {
289                     final List<String> variables = new LinkedList<>();
290                     if (ind.getVariables() != null
291                             && !ind.getVariables().isEmpty()) {
292                         ind.getVariables().forEach(variable -> variables.add(variable.getName()));
293                         Collections.sort(variables);
294                         allVariables.addAll(variables);
295                     }
296                     final List<String> parameters = new LinkedList<>();
297                     if (ind.getParameters() != null
298                             && !ind.getParameters().isEmpty()) {
299                         ind.getParameters().forEach(param -> parameters.add(param.getId()));
300                         Collections.sort(parameters);
301                     }
302                     String unit = "";
303                     if (ind.getUnit() != null) {
304                         List<LocalizedString> symbols = ind.getUnit().getSymbols();
305                         if (symbols != null && !symbols.isEmpty()) {
306                             unit = LocalizedString.getString(symbols, languageCode);
307                         }
308                         if (unit == null || unit.isBlank()) {
309                             final List<LocalizedString> labels = ind.getUnit().getLabels();
310                             if (labels != null && !labels.isEmpty()) {
311                                 unit = LocalizedString.getString(labels, languageCode);
312                             }
313                         }
314                     }
315                     // affichage des références des notes de l'indicateurs
316                     final List<String> notes = new LinkedList<>();
317                     if (ind.getNotes() != null && !ind.getNotes().isEmpty()) {
318                         ind.getNotes().forEach(note -> {
319                             final String anchor = note.getId();
320                             notes.add("<a href='#" + anchor + "'>" + note.getId() + "</a>");
321                         });
322                     }
323                     writeLn(mdWriter, ind.getId(), ind.getName(languageCode),
324                             ind.getDescription(languageCode), String.join(", ", variables),
325                             String.join(", ", parameters), unit, String.join(", ", notes));
326                 }
327             }
328 
329             mdWriter.write("""
330 
331                            ###\s""" + i18n.get("markdown.parameters") + "\n"
332                     + "| " + i18n.get("markdown.id") + " | " + i18n.get("markdown.description") + " |\n"
333                     + "|:---|:------------|\n");
334 
335             for (final Parameter param : knowledge.getParameters()) {
336                 writeLn(mdWriter, param.getId(), param.getDescription(languageCode));
337             }
338 
339             mdWriter.write("""
340 
341                            ###\s""" + i18n.get("markdown.variables") + "\n"
342                     + "| " + i18n.get("markdown.id") + " | " + i18n.get("markdown.description") + " |\n"
343                     + "|:---|:------------|\n");
344             allVariables.stream().sorted().forEach(variable -> {
345                 try {
346                     mdWriter.write("| ");
347                     mdWriter.write(variable);
348                     mdWriter.write(" | ");
349                     mdWriter.write(i18n.get("Variable." + variable.toUpperCase() + ".description"));
350                     mdWriter.write(" |\n");
351                 } catch (final IOException ex) {
352                     LOGGER.catching(ex);
353                 }
354             });
355 
356             // Ecriture de l'ensemble des notes présentes
357             if (knowledge.getNotes() != null && !knowledge.getNotes().isEmpty()) {
358                 mdWriter.write("""
359 
360                                ###\s""" + i18n.get("markdown.notes") + "\n"
361                         + "| " + i18n.get("markdown.reference") + " | " + i18n.get("markdown.description") + " |\n"
362                         + "|:---|:------------|\n");
363                 for (final Note note : knowledge.getNotes()) {
364                     final String anchor;
365 
366                     // si il s'agit d'un DOI, on affiche le lien
367                     final String id = note.getId();
368                     if (StringUtils.isDoiRef(id)) {
369                         anchor = String.format(
370                                 "<a id=\"%1$s\" href=\"https://doi.org/%1$s\" target=\"_blank\">%1$s</a>",
371                                 id);
372                     } else {
373                         anchor = String.format("<a id=\"%1$s\">%1$s</a>", id);
374                     }
375 
376                     writeLn(mdWriter, anchor, note.getDescription());
377                 }
378             }
379 
380             mdWriter.write("\n\n[^1]: " + i18n.get("markdown.unit.footnote"));
381         }
382     }
383 
384     /**
385      * Whatever is the locale, the file is the same.
386      *
387      * @param path output file path
388      * @throws IOException
389      */
390     public void writeParametersCsvFiles(final Path path) throws IOException {
391         LOGGER.trace(path);
392         try (BufferedWriter paramWriter = new Utf8BufferedWriter(path)) {
393             paramWriter.write("id;description_fr;description_en\n");
394             for (final Parameter param : knowledge.getParameters()) {
395                 paramWriter.write(param.getId());
396                 paramWriter.write(";");
397                 paramWriter.write(param.getDescription("fr"));
398                 paramWriter.write(";");
399                 paramWriter.write(param.getDescription("en"));
400                 paramWriter.write("\n");
401             }
402         }
403     }
404 }