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