001/*
002 *    GeoAPI - Java interfaces for OGC/ISO standards
003 *    Copyright © 2012-2024 Open Geospatial Consortium, Inc.
004 *    http://www.geoapi.org
005 *
006 *    Licensed under the Apache License, Version 2.0 (the "License");
007 *    you may not use this file except in compliance with the License.
008 *    You may obtain a copy of the License at
009 *
010 *        http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *    Unless required by applicable law or agreed to in writing, software
013 *    distributed under the License is distributed on an "AS IS" BASIS,
014 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 *    See the License for the specific language governing permissions and
016 *    limitations under the License.
017 */
018package org.opengis.test.report;
019
020import java.io.*;
021import java.util.List;
022import java.util.ArrayList;
023import java.util.Properties;
024import java.util.Collection;
025import java.util.Collections;
026
027import org.opengis.util.FactoryException;
028import org.opengis.referencing.IdentifiedObject;
029import org.opengis.referencing.AuthorityFactory;
030import org.opengis.referencing.crs.CoordinateReferenceSystem;
031
032import org.opengis.metadata.Identifier;
033import org.opengis.referencing.crs.CRSAuthorityFactory;
034
035
036/**
037 * Generates a list of object identified by authority codes for a given
038 * {@linkplain AuthorityFactory authority factory}.
039 *
040 * <p>This class recognizes the following property values. Note that default values are
041 * automatically generated for the {@code "COUNT.*"} and {@code "PERCENT.*"} entries.</p>
042 *
043 * <table class="ogc">
044 *   <caption>Report properties</caption>
045 *   <tr><th>Key</th>                            <th>Remarks</th>   <th>Meaning</th></tr>
046 *   <tr><td>{@code TITLE}</td>                  <td></td>          <td>Title of the web page to produce.</td></tr>
047 *   <tr><td>{@code DESCRIPTION}</td>            <td>optional</td>  <td>Description to write after the introductory paragraph.</td></tr>
048 *   <tr><td>{@code OBJECTS.KIND}</td>           <td></td>          <td>Kind of objects listed in the page (e.g. <q>Coordinate Reference Systems</q>).</td></tr>
049 *   <tr><td>{@code FACTORY.NAME}</td>           <td></td>          <td>The name of the authority factory.</td></tr>
050 *   <tr><td>{@code FACTORY.VERSION}</td>        <td></td>          <td>The version of the authority factory.</td></tr>
051 *   <tr><td>{@code FACTORY.VERSION.SUFFIX}</td> <td>optional</td>  <td>An optional text to write after the factory version (in the main text only).</td></tr>
052 *   <tr><td>{@code PRODUCT.NAME}</td>           <td></td>          <td>Name of the product for which the report is generated.</td></tr>
053 *   <tr><td>{@code PRODUCT.VERSION}</td>        <td></td>          <td>Version of the product for which the report is generated.</td></tr>
054 *   <tr><td>{@code PRODUCT.VERSION.SUFFIX}</td> <td>optional</td>  <td>An optional text to write after the product version (in the main text only).</td></tr>
055 *   <tr><td>{@code PRODUCT.URL}</td>            <td></td>          <td>URL where more information is available about the product.</td></tr>
056 *   <tr><td>{@code JAVADOC.GEOAPI}</td>         <td>predefined</td><td>Base URL of GeoAPI javadoc.</td></tr>
057 *   <tr><td>{@code COUNT.OBJECTS}</td>          <td>automatic</td> <td>Number of identified objects.</td></tr>
058 *   <tr><td>{@code PERCENT.VALIDS}</td>         <td>automatic</td> <td>Percentage of objects successfully created (i.e. having no {@linkplain Row#hasError error}).</td></tr>
059 *   <tr><td>{@code PERCENT.ANNOTATED}</td>      <td>automatic</td> <td>Percentage of objects having an {@linkplain Row#annotation annotation}.</td></tr>
060 *   <tr><td>{@code PERCENT.DEPRECATED}</td>     <td>automatic</td> <td>Percentage of {@linkplain Row#isDeprecated deprecated} objects.</td></tr>
061 *   <tr><td>{@code FILENAME}</td>               <td>predefined</td><td>Name of the file to create if the {@link #write(File)} argument is a directory.</td></tr>
062 * </table>
063 *
064 * <p><b>How to use this class:</b></p>
065 * <ol>
066 *   <li>Create a {@link Properties} map with the values documented in the above table.
067 *       Default values exist for many keys, but may depend on the environment.
068 *       It is safer to specify values explicitly when they are known, except the <i>automatic</i> ones.</li>
069 *   <li>Create a new {@code AuthorityCodesReport} with the above properties map given to the constructor.</li>
070 *   <li>Invoke one of the {@link #add(CRSAuthorityFactory) add(…)} methods for the factory of identified objects
071 *       to include in the HTML page.</li>
072 *   <li>Invoke {@link #write(File)}.</li>
073 * </ol>
074 *
075 * @author Martin Desruisseaux (Geomatys)
076 * @version 3.1
077 *
078 * @since 3.1
079 */
080public class AuthorityCodesReport extends Report {
081    /**
082     * A single row in the table produced by {@link AuthorityCodesReport}. Instances of this class are created by the
083     * {@link AuthorityCodesReport#createRow(String, IdentifiedObject) AuthorityCodesReport.createRow(…)} methods.
084     * Subclasses of {@code AuthorityCodesReport} can override those methods in order to modify the content of a row.
085     *
086     * <p>Every {@link String} fields in this class must be valid HTML. If some text is expected to print
087     * {@code <} or {@code >} characters, then those characters need to be escaped to their HTML entities.</p>
088     *
089     * <p>Content of each {@code Row} instance is written in the following order:</p>
090     * <ol>
091     *   <li>{@link #annotation} if explicitly set (the default is none).</li>
092     *   <li>{@link #code}</li>
093     *   <li>{@link #name}</li>
094     *   <li>{@link #remark}</li>
095     * </ol>
096     *
097     * <p>Other attributes ({@link #isSectionHeader}, {@link #isDeprecated} and {@link #hasError})
098     * are not directly written in the table, but affect their styling.</p>
099     *
100     * @author Martin Desruisseaux (Geomatys)
101     * @version 3.1
102     *
103     * @see AuthorityCodesReport#createRow(String, IdentifiedObject)
104     * @see AuthorityCodesReport#createRow(String, FactoryException)
105     *
106     * @since 3.1
107     */
108    protected static class Row implements Comparable<Row> {
109        /**
110         * The authority code in HTML.
111         */
112        public String code;
113
114        /**
115         * The object name in HTML, or {@code null} if none. By default, this field is set to the value of
116         * <code>{@linkplain IdentifiedObject#getName()}.{@linkplain Identifier#getCode() getCode()}</code>.
117         *
118         * <p>Users can override {@link AuthorityCodesReport#createRow(String, IdentifiedObject)} if they
119         * wish to change the value of this field.</p>
120         */
121        public String name;
122
123        /**
124         * A remark in HTML to display after the name, or {@code null} if none.
125         * By default, this field is set to one of the following values:
126         *
127         * <ul>
128         *   <li>If the object creation was successful, the {@link IdentifiedObject#getRemarks()}
129         *       localized to the {@linkplain AuthorityCodesReport#getLocale() report locale}.</li>
130         *   <li>Otherwise, the {@link FactoryException} localized message.</li>
131         * </ul>
132         *
133         * <p>Users can override {@link AuthorityCodesReport#createRow(String, IdentifiedObject)}
134         * or {@link AuthorityCodesReport#createRow(String, FactoryException)} if they wish to change
135         * the value of this field.</p>
136         */
137        public String remark;
138
139        /**
140         * A small symbol to put before the {@linkplain #code} and {@linkplain #name}, or 0 (the default) if none.
141         * Implementations can use this field for putting a mark before objects having some particular characteristics,
142         * for example a CRS having unusual axes order.
143         */
144        public char annotation;
145
146        /**
147         * {@code true} if this row should actually be used as a section header. Users can insert rows
148         * with this flag set to {@code true} if they wish to split the large table is smaller sections.
149         */
150        public boolean isSectionHeader;
151
152        /**
153         * {@code true} if this authority code is deprecated, or {@code false} otherwise.
154         */
155        public boolean isDeprecated;
156
157        /**
158         * {@code true} if an exception occurred while creating the identified object.
159         * If {@code true}, then the {@link #remark} field will contains the exception localized message.
160         */
161        public boolean hasError;
162
163        /**
164         * Creates a new row with all fields initialized to {@code null} or {@code false}.
165         */
166        public Row() {
167        }
168
169        /**
170         * Writes this row to the given stream.
171         *
172         * @param  out        where to write this row.
173         * @param  highlight  whether to highlight this row.
174         * @throws IOException if an error occurred while writing this row.
175         */
176        final void write(final Appendable out, final boolean highlight) throws IOException {
177            if (isSectionHeader) {
178                out.append("<tr class=\"separator\"><td colspan=\"4\">").append(name).append("</td></tr>");
179                return;
180            }
181            out.append("<tr");                    if (highlight)       out.append(" class=\"HL\"");
182            out.append("><td class=\"narrow\">"); if (annotation != 0) out.append(annotation);
183            out.append("</td><td><code>");        if (isDeprecated)    out.append("<del>");
184                                                  if (code != null)    out.append(code);
185                                                  if (isDeprecated)    out.append("</del>");
186            out.append("</code></td><td>");       if (name != null)    out.append(name);
187            out.append("</td><td");               if (hasError)        out.append(" class=\"error\"");
188                                             else if (isDeprecated)    out.append(" class=\"warning\"");
189            out.append('>');                      if (remark != null)  out.append(remark);
190            out.append("</td></tr>");
191        }
192
193        /**
194         * Compares this row with the given one for order. The default implementation
195         * {@linkplain String#split(String) splits} the code spaces (or scopes) from the
196         * codes using the {@code ":"} separator, then compares each elements. This method tries
197         * to compare the elements as numeric values if possible (i.e. 4326 is less than 27561).
198         * If the codes cannot be compared as numerical values, then they are compared as strings
199         * using a {@linkplain String#CASE_INSENSITIVE_ORDER case-insensitive comparator}.
200         *
201         * <p>Subclasses can override this method if they want a different rows ordering.</p>
202         *
203         * @param  o  the other row to compare with this row.
204         * @return -1 for sorting this row before the given row, +1 for sorting it after,
205         *         or 0 if the two rows have equal ordering.
206         */
207        @Override
208        public int compareTo(final Row o) {
209            return IdentifiedObjects.compare(code.split(IdentifiedObjects.SEPARATOR),
210                                           o.code.split(IdentifiedObjects.SEPARATOR));
211        }
212
213        /**
214         * Returns a string representation of this row, for debugging purpose only.
215         *
216         * @return an arbitrary string representation of this row.
217         */
218        @Override
219        public String toString() {
220            final StringBuilder buffer = new StringBuilder(64);
221            try {
222                write(buffer, false);
223            } catch (IOException e) {
224                throw new AssertionError(e);        // Should never happen.
225            }
226            return buffer.toString();
227        }
228    }
229
230    /**
231     * The list of objects identified by the codes declared by the authority factory. Elements
232     * are added in this list by any of the {@link #add(CRSAuthorityFactory) add} methods.
233     */
234    protected final List<Row> rows;
235
236    /**
237     * Creates a new report generator using the given property values.
238     * See the class javadoc for a list of expected values.
239     *
240     * @param properties  the property values, or {@code null} for the default values.
241     */
242    public AuthorityCodesReport(final Properties properties) {
243        super(properties);
244        rows = new ArrayList<>(1024);
245        defaultProperties.setProperty("TITLE", "Authority codes for ${OBJECTS.KIND}");
246        defaultProperties.setProperty("OBJECTS.KIND", "Identified Objects");
247        defaultProperties.setProperty("FACTORY.VERSION.SUFFIX", "");
248        defaultProperties.setProperty("PRODUCT.VERSION.SUFFIX", "");
249    }
250
251    /**
252     * Sets the default product name and factory name.
253     *
254     * @param  factory  the factory to set.
255     */
256    private void setDefault(final AuthorityFactory factory) {
257        setVendor("PRODUCT", factory.getVendor());
258        setVendor("FACTORY", factory.getAuthority());
259    }
260
261    /**
262     * Adds the given row to the {@link #rows} list, of non-null.
263     *
264     * @param  row  the row to add if non-null.
265     */
266    private void add(final Row row) {
267        if (row != null) {
268            rows.add(row);
269        }
270    }
271
272    /**
273     * Adds the Coordinate Reference Systems identified by all codes available from the given CRS authority factory.
274     * This method performs the following steps:
275     *
276     * <ul>
277     *   <li>Get the list of available codes for type {@link CoordinateReferenceSystem}
278     *     with {@link CRSAuthorityFactory#getAuthorityCodes(Class)}.</li>
279     *   <li>For each code, try to instantiate an object with
280     *     {@link CRSAuthorityFactory#createCoordinateReferenceSystem(String)}, then:
281     *     <ul>
282     *       <li>In case of success, invoke {@link #createRow(String, IdentifiedObject)};</li>
283     *       <li>In case of failure, invoke {@link #createRow(String, FactoryException)}.</li>
284     *     </ul>
285     *   </li>
286     *   <li>If the {@code createRow(…)} method returned a non-null
287     *       instance, add the created row to the {@link #rows} list.</li>
288     * </ul>
289     *
290     * Subclasses can override the above-cited {@code createRow(…)}
291     * methods in order to customize the table content.
292     *
293     * @param  factory  the factory from which to get Coordinate Reference System instances.
294     * @throws FactoryException if a non-recoverable error occurred while querying the factory.
295     */
296    public void add(final CRSAuthorityFactory factory) throws FactoryException {
297        setDefault(factory);
298        defaultProperties.setProperty("TITLE", "Authority codes for Coordinate Reference Systems");
299        defaultProperties.setProperty("OBJECTS.KIND", "Coordinate Reference Systems (CRS)");
300        defaultProperties.setProperty("FILENAME", "CRS-Codes.html");
301        final Collection<String> codes = factory.getAuthorityCodes(CoordinateReferenceSystem.class);
302        for (final String code : codes) {
303            try {
304                add(createRow(code, factory.createCoordinateReferenceSystem(code)));
305            } catch (FactoryException exception) {
306                add(createRow(code, exception));
307            }
308        }
309    }
310
311    /**
312     * Returns a new {@link Row} instance. Subclasses can override this method if they wish to
313     * instantiate a subclass of {@code Row}.
314     *
315     * @return the new, initially empty, {@code Row} instance.
316     */
317    protected Row newRow() {
318        return new Row();
319    }
320
321    /**
322     * Creates a new row for the given authority code and identified object.
323     * Subclasses can override this method in order to customize the table content.
324     *
325     * @param  code    the authority code of the created object.
326     * @param  object  the object created from the given authority code.
327     * @return the created row, or {@code null} if the row should be ignored.
328     */
329    protected Row createRow(final String code, final IdentifiedObject object) {
330        final Row row = newRow();
331        row.code = escape(code);
332        if (object != null) {
333            final Identifier name = object.getName();
334            if (name != null) {
335                row.name = escape(name.getCode());
336            }
337            row.remark = escape(toString(object.getRemarks()));
338        }
339        return row;
340    }
341
342    /**
343     * Creates a new row for the given authority code and exception.
344     * Subclasses can override this method in order to customize the table content.
345     *
346     * @param  code       the authority code of the object to create.
347     * @param  exception  the exception that occurred while creating the identified object.
348     * @return the created row, or {@code null} if the row should be ignored.
349     */
350    protected Row createRow(final String code, final FactoryException exception) {
351        final Row row = newRow();
352        row.code = escape(code);
353        row.hasError = true;
354        if (exception != null) {
355            row.remark = escape(exception.getLocalizedMessage());
356            if (row.remark == null) {
357                row.remark = escape(exception.toString());
358            }
359        }
360        return row;
361    }
362
363    /**
364     * Sorts the rows before to {@linkplain #write(File) write} them.
365     * The default implementation sort the rows by their {@linkplain Row#compareTo natural ordering}.
366     * Subclasses can override this method if they want to sort the rows otherwise,
367     * or if they want to add or remove rows before or after the sorting.
368     */
369    protected void sortRows() {
370        Collections.sort(rows);
371    }
372
373    /**
374     * Formats the identified objects as a HTML page in the given file.
375     *
376     * @param  destination  the file to generate.
377     * @return the given {@code destination} file.
378     * @throws IOException if an error occurred while writing the report.
379     */
380    @Override
381    public File write(File destination) throws IOException {
382        final int numRows = rows.size();
383        int numValids = 0, numAnnotations = 0, numDeprecated = 0;
384        for (final Row row : rows) {
385            if (!row.hasError)       numValids++;
386            if (row.annotation != 0) numAnnotations++;
387            if (row.isDeprecated)    numDeprecated++;
388        }
389        defaultProperties.setProperty("COUNT.OBJECTS",      Integer.toString(numRows));
390        defaultProperties.setProperty("PERCENT.VALIDS",     Integer.toString(100 * numValids / numRows) + '%');     // Really want rounding toward 0.
391        defaultProperties.setProperty("PERCENT.ANNOTATED",  Integer.toString(Math.round(100f * numAnnotations / numRows)) + '%');
392        defaultProperties.setProperty("PERCENT.DEPRECATED", Integer.toString(Math.round(100f * numDeprecated  / numRows)) + '%');
393        sortRows();
394        /*
395         * The above initialization needs to be done before to start
396         * the actual content writing. Now we can write the HTML table.
397         */
398        destination = toFile(destination);
399        filter("AuthorityCodes.html", destination);
400        return destination;
401    }
402
403    /**
404     * Invoked by {@link Report} every time a {@code ${FOO}} occurrence is found.
405     *
406     * @throws IOException if an error occurred during the copy.
407     */
408    @Override
409    final void writeContent(final BufferedWriter out, final String key) throws IOException {
410        if (!"CONTENT".equals(key)) {
411            super.writeContent(out, key);
412            return;
413        }
414        int c = 0;
415        for (final Row row : rows) {
416            // Do not put indentation, because there is a lot of rows.
417            // 8 spaces in 4933 rows waste 39 kb (about 5% of the total file size).
418            row.write(out, (c & 2) != 0);
419            out.newLine();
420            c++;
421            if (row.isSectionHeader) {
422                c = 0;
423            }
424        }
425    }
426}