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.File;
035import java.io.IOException;
036import java.io.BufferedWriter;
037import java.util.Set;
038import java.util.Map;
039import java.util.List;
040import java.util.ArrayList;
041import java.util.Collections;
042import java.util.LinkedHashSet;
043import java.util.LinkedHashMap;
044import java.util.Properties;
045
046import org.opengis.util.GenericName;
047import org.opengis.metadata.Identifier;
048import org.opengis.parameter.GeneralParameterDescriptor;
049import org.opengis.parameter.ParameterDescriptorGroup;
050import org.opengis.referencing.IdentifiedObject;
051import org.opengis.referencing.operation.SingleOperation;
052import org.opengis.referencing.operation.OperationMethod;
053import org.opengis.referencing.operation.MathTransformFactory;
054
055
056/**
057 * Generates a list of operations (typically map projections) and their parameters.
058 * The operations are described by instances of an {@link IdentifiedObject} subtype,
059 * for example coordinates {@link OperationMethod}. Each operation can be associated
060 * to a {@link ParameterDescriptorGroup} instance. Those elements can be
061 * {@linkplain #add(IdentifiedObject, ParameterDescriptorGroup) added individually}
062 * in the {@linkplain #rows} list. Alternatively, a convenience method can be used
063 * for adding all operation methods available from a given {@link MathTransformFactory}.
064 *
065 * <p>This class recognizes the following property values:</p>
066 *
067 * <table class="ogc">
068 *   <caption>Report properties</caption>
069 *   <tr><th>Key</th>                    <th align="center">Remarks</th>   <th>Meaning</th></tr>
070 *   <tr><td>{@code TITLE}</td>          <td align="center">&nbsp;</td>    <td>Title of the web page to produce.</td></tr>
071 *   <tr><td>{@code DESCRIPTION}</td>    <td align="center">optional</td>  <td>Description to write after the introductory paragraph.</td></tr>
072 *   <tr><td>{@code OBJECTS.KIND}</td>   <td align="center">&nbsp;</td>    <td>Kind of objects listed in the page (e.g. <cite>"Operation Methods"</cite>).</td></tr>
073 *   <tr><td>{@code PRODUCT.NAME}</td>   <td align="center">&nbsp;</td>    <td>Name of the product for which the report is generated.</td></tr>
074 *   <tr><td>{@code PRODUCT.VERSION}</td><td align="center">&nbsp;</td>    <td>Version of the product for which the report is generated.</td></tr>
075 *   <tr><td>{@code PRODUCT.URL}</td>    <td align="center">&nbsp;</td>    <td>URL where more information is available about the product.</td></tr>
076 *   <tr><td>{@code JAVADOC.GEOAPI}</td> <td align="center">predefined</td><td>Base URL of GeoAPI javadoc.</td></tr>
077 *   <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>
078 * </table>
079 *
080 * <p><b>How to use this class:</b></p>
081 * <ul>
082 *   <li>Create a {@link Properties} map with the values documented in the above table. Default
083 *       values exist for many keys, but may depend on the environment. It is safer to specify
084 *       values explicitly when they are known.</li>
085 *   <li>Create a new {@code OperationParametersReport} with the above properties map
086 *       given to the constructor.</li>
087 *   <li>Invoke one of the {@link #add(IdentifiedObject, ParameterDescriptorGroup) add} method
088 *       for each operation or factory to include in the HTML page.</li>
089 *   <li>Invoke {@link #write(File)}.</li>
090 * </ul>
091 *
092 * @author Martin Desruisseaux (Geomatys)
093 * @version 3.1
094 *
095 * @since 3.1
096 */
097public class OperationParametersReport extends Report {
098    /**
099     * A single row in the table produced by {@link OperationParametersReport}.
100     * Instances of this class are created by the {@link OperationParametersReport#createRow
101     * OperationParametersReport.createRow(…)} method. Subclasses of {@code OperationParametersReport}
102     * can override that methods in order to modify the content of a row.
103     *
104     * <p>Every {@link String} fields in this class can contain HTML elements, especially the
105     * {@linkplain #names} values. If some text is expected to print {@code <} or {@code >}
106     * characters, then those characters need to be escaped to their HTML entities.</p>
107     *
108     * @author Martin Desruisseaux (Geomatys)
109     * @version 3.1
110     *
111     * @see OperationParametersReport#createRow(IdentifiedObject, ParameterDescriptorGroup, Set)
112     *
113     * @since 3.1
114     */
115    protected static class Row implements Comparable<Row> {
116        /**
117         * An optional user category, or {@code null} if none. If non-null, this category will be
118         * formatted as a single row in the HTML table before all subsequent {@code Row} instances
119         * of the same category.
120         *
121         * <p>The default value is {@code null} in every cases. Subclasses of {@link OperationParametersReport}
122         * can modify this value in order to classify operations by category. For example subclasses
123         * may use this value for classifying {@link OperationMethod} instances according the kind
124         * of map projection (<cite>planar</cite>, <cite>cylindrical</cite>, <cite>conic</cite>).</p>
125         */
126        public String category;
127
128        /**
129         * The {@link IdentifiedObject} name, used only for {@link #compareTo(Row)} implementation.
130         * This field is not used for defining the row content.
131         */
132        private final Identifier name;
133
134        /**
135         * The names or aliases to write on the table row. Each entry will be formatted in a
136         * single table cell. The column of the cell is determined by the key, and the content
137         * is determined by the value. More specifically:
138         *
139         * <ul>
140         *   <li>{@linkplain Map#keySet() Map keys} are the {@linkplain Identifier#getCodeSpace()
141         *   code spaces} or {@linkplain GenericName#scope() scopes} of the name or aliases.</li>
142         *
143         *   <li>{@linkplain Map#values() Map values} are the {@linkplain Identifier#getCode()
144         *   codes} or {@linkplain GenericName#toInternationalString() string representations} of the name
145         *   or aliases.</li>
146         * </ul>
147         *
148         * <p>The values may contain HTML elements. In particular:</p>
149         * <ul>
150         *   <li>{@code <em>…</em>} for {@linkplain IdentifiedObject#getName() primary names}.</li>
151         *   <li>{@code <del>…</del>} for deprecated objects (need to be added by the user).</li>
152         * </ul>
153         */
154        public final Map<String,String[]> names;
155
156        /**
157         * The operation parameters or the parameter sub-groups, or {@code null} if not applicable.
158         * If this row describes an operation, then the content of this list is derived from the
159         * values returned by {@link ParameterDescriptorGroup#descriptors()}. If this row describes
160         * a parameter, then this list will contain the sub-groups (if any).
161         *
162         * <p><b>Note:</b> subgroups are not yet supported.</p>
163         */
164        public List<Row> parameters;
165
166        /**
167         * Creates a row to be show on the HTML page.
168         *
169         * @param  object      the operation or parameter to show on the HTML page.
170         * @param  codeSpaces  the code spaces for which to get the name and aliases.
171         */
172        public Row(final IdentifiedObject object, final Set<String> codeSpaces) {
173            name  = object.getName();
174            names = new LinkedHashMap<>();
175            for (final String cs : codeSpaces) {
176                final Map<String,Boolean> toCopy = IdentifiedObjects.getNameAndAliases(object, cs);
177                final int size = toCopy.size();
178                if (size != 0) {
179                    int i=0;
180                    final String[] array = new String[size];
181                    for (final Map.Entry<String,Boolean> entry : toCopy.entrySet()) {
182                        String name = escape(entry.getKey());
183                        if (entry.getValue()) {
184                            name = "<em>" + name + "</em>";
185                        }
186                        array[i++] = name;
187                    }
188                    if (names.put(cs, array) != null) {
189                        throw new AssertionError(cs);                       // Should never happen.
190                    }
191                }
192            }
193        }
194
195        /**
196         * Creates a new row initialized to a shallow copy of the given row.
197         * The {@link Map} and {@link List} collections are copied, but the
198         * content of those collections are not cloned.
199         *
200         * @param toCopy  the row to copy.
201         */
202        public Row(final Row toCopy) {
203            category = toCopy.category;
204            name     = toCopy.name;
205            names    = new LinkedHashMap<>(toCopy.names);
206            if (toCopy.parameters != null) {
207                parameters = new ArrayList<>(toCopy.parameters);
208            }
209        }
210
211        /**
212         * Compares this row with the given object for order. This method is used for sorting
213         * the operations in the order to be show on the HTML output page.
214         *
215         * <p>The default implementation compare that {@linkplain #category} first - this is
216         * needed in order to ensure that operations of the same category are grouped. Then,
217         * this method compares {@linkplain IdentifiedObject#getName() object names} components
218         * in the following order: {@linkplain Identifier#getCode() code},
219         * {@linkplain Identifier#getCodeSpace() code space} and
220         * {@linkplain Identifier#getVersion() version}.</p>
221         *
222         * <p>Subclasses can override this method if they want a different ordering
223         * on the HTML page.</p>
224         *
225         * @param  o  the other row to compare with this row.
226         * @return -1 if {@code this} should appears before {@code o}, -1 for the converse,
227         *         or 0 if this method can not determine an ordering for the given object.
228         */
229        @Override
230        public int compareTo(final Row o) {
231            int c = IdentifiedObjects.compare(category, o.category);
232            if (c == 0) {
233                c = IdentifiedObjects.compare(name, o.name);
234            }
235            return c;
236        }
237
238        /**
239         * Returns a string representation of this row, for debugging purpose only.
240         *
241         * @return an arbitrary string representation of this row.
242         */
243        @Override
244        public String toString() {
245            final StringBuilder buffer = new StringBuilder(64);
246            try {
247                write(buffer, names.keySet().toArray(new String[names.size()]), false, false, false);
248            } catch (IOException e) {
249                throw new AssertionError(e);                                // Should never happen.
250            }
251            return buffer.toString();
252        }
253
254        /**
255        * Writes a single row with the names of the given objects.
256        *
257        * @param  out         where to write the content.
258        * @param  codeSpaces  the code spaces to use in columns, typically {@link #getCodeSpaces()}.
259        * @param  isGroup     {@code true} if formatting a group, or {@code false} for a parameter.
260        * @param  isHead      {@code true} if formatting the first group of parameter values in a section.
261        * @param  isTail      {@code true} if formatting the last parameter value in a group.
262        * @throws IOException if an error occurred while writing the content.
263        */
264        final void write(final Appendable out, final String[] codeSpaces,
265                final boolean isGroup, final boolean isHead, final boolean isTail) throws IOException
266        {
267            out.append("<tr");
268            writeClassAttribute(out,
269                    isGroup  ? "groupName" : null,
270                    isHead   ? "groupHead" : null,
271                    isTail   ? "groupTail" : null);
272            out.append('>');
273            for (int i=0; i<codeSpaces.length;) {
274                final String cs = codeSpaces[i];
275                final String[] codes = names.get(cs);
276                /*
277                 * If the next columns are empty, allow the current column to use their space.
278                 * This allow a more compact table since EPSG names may be quite long, and in
279                 * many cases have no corresponding names in other code spaces.
280                 */
281                int colspan = 1;
282                while (++i < codeSpaces.length) {
283                    if (names.get(codeSpaces[i]) != null) {
284                        break;
285                    }
286                    colspan++;
287                }
288                out.append("<td");
289                if (colspan != 1) {
290                    out.append(" colspan=\"");
291                    out.append(Integer.toString(colspan));
292                    out.append('"');
293                }
294                out.append('>');
295                /*
296                 * Write the parameter name. Typically there is only one name, since we are
297                 * formatting the names for only one code space. However in some few cases,
298                 * we still have many names declared by the same authority. The other names
299                 * are typically legacy names. In such case, we will put each additional
300                 * name on its own line in the same cell.
301                 */
302                boolean hasMore = false;
303                if (codes != null) {
304                    // Intentionally no enclosing <ul>.
305                    if (!isGroup) out.append("<li>");
306                    for (final String name : codes) {
307                        if (hasMore) out.append("<br>");
308                        out.append(name);
309                        hasMore = true;
310                    }
311                    if (!isGroup) out.append("</li>");
312                }
313                out.append("</td>");
314            }
315            out.append("</tr>");
316        }
317    }
318
319    /**
320     * The operations to publish in the HTML report.
321     *
322     * @see #add(IdentifiedObject, ParameterDescriptorGroup)
323     * @see #add(MathTransformFactory)
324     */
325    protected final List<Row> rows;
326
327    /**
328     * The number of indentation spaces.
329     */
330    private int indentation;
331
332    /**
333     * Creates a new report generator using the given property values.
334     * See the class javadoc for a list of expected values.
335     *
336     * @param properties  the property values, or {@code null} for the default values.
337     */
338    public OperationParametersReport(final Properties properties) {
339        super(properties);
340        rows = new ArrayList<>();
341        defaultProperties.setProperty("TITLE", "Supported ${OBJECTS.KIND}");
342    }
343
344    /**
345     * Adds an operation to be show on the HTML page. The default implementation performs the
346     * following steps:
347     *
348     * <ul>
349     *   <li>Get the set of all code spaces or scopes found in the given {@code operation}.</li>
350     *   <li>Delegates to {@link #createRow createRow(…)} with the above set. This means that
351     *       any parameter names defined in an other scope will be ignored.</li>
352     *   <li>Add the new row to the {@linkplain #rows} list if non-null.</li>
353     * </ul>
354     *
355     * @param  operation   the operation to show on the HTML page.
356     * @param  parameters  the operation parameters, or {@code null} if none.
357     */
358    public void add(final IdentifiedObject operation, final ParameterDescriptorGroup parameters) {
359        final Map<String, Boolean> codeSpaces = new LinkedHashMap<>(8);
360        IdentifiedObjects.getCodeSpaces(operation, codeSpaces);
361        final Row group = createRow(operation, parameters, codeSpaces.keySet());
362        if (group != null) {
363            rows.add(group);
364        }
365    }
366
367    /**
368     * Convenience method adding all {@linkplain MathTransformFactory#getAvailableMethods(Class)
369     * available methods} from the given factory. Each {@linkplain OperationMethod coordinate
370     * operation method} is added to the {@linkplain #rows} list as below:
371     *
372     * <blockquote><code>{@linkplain #add(IdentifiedObject, ParameterDescriptorGroup)
373     * add}(method, method.{@linkplain OperationMethod#getParameters() getParameters()});</code></blockquote>
374     *
375     * @param  factory  the factory for which to add available methods.
376     */
377    public void add(final MathTransformFactory factory) {
378        defaultProperties.setProperty("OBJECTS.KIND", "Coordinate Operations");
379        defaultProperties.setProperty("FILENAME", "CoordinateOperations.html");
380        setVendor("PRODUCT", factory.getVendor());
381        final Set<OperationMethod> operations = factory.getAvailableMethods(SingleOperation.class);
382        final int previousCount = rows.size();
383        for (final OperationMethod operation : operations) {
384            add(operation, operation.getParameters());
385            progress(previousCount + rows.size(),
386                     previousCount + operations.size());
387        }
388    }
389
390    /**
391     * Creates a new row for the given operation and parameters. This method is invoked by the
392     * {@link #add(IdentifiedObject, ParameterDescriptorGroup) add(…)} method when a new row
393     * needs to be created, either for an operation or for one of its parameters.
394     *
395     * <p>The default implementation instantiate a new {@link Row} with the given operation and
396     * code spaces. Then, if the given {@code parameters} argument is non-null, this method
397     * iterates over all parameter descriptor and invokes this method recursively for creating
398     * their rows.</p>
399     *
400     * @param  operation   the operation.
401     * @param  parameters  the operation parameters, or {@code null} if none.
402     * @param  codeSpaces  the code spaces for which to get the name and aliases.
403     * @return the new row, or {@code null} if none.
404     */
405    protected Row createRow(final IdentifiedObject operation, final ParameterDescriptorGroup parameters, final Set<String> codeSpaces) {
406        final Row row = new Row(operation, codeSpaces);
407        if (parameters != null) {
408            final List<GeneralParameterDescriptor> descriptors = parameters.descriptors();
409            for (final GeneralParameterDescriptor desc : descriptors) {
410                final Row child = createRow(desc, (desc instanceof ParameterDescriptorGroup) ?
411                        (ParameterDescriptorGroup) desc : null, codeSpaces);
412                if (child != null) {
413                    if (row.parameters == null) {
414                        row.parameters = new ArrayList<>(descriptors.size());
415                    }
416                    row.parameters.add(child);
417                }
418            }
419        }
420        return row;
421    }
422
423    /**
424     * Returns the HTML text to use as a column header for each
425     * {@linkplain Identifier#getCodeSpace() code spaces} or
426     * {@linkplain GenericName#scope() scopes}. The columns will be show in iteration order.
427     *
428     * @return the name of all code spaces or scopes. Some typical values are {@code "EPSG"},
429     *         {@code "OGC"}, {@code "ESRI"}, {@code "GeoTIFF"} or {@code "NetCDF"}.
430     */
431    private String[] getColumnHeaders() {
432        final Set<String> codeSpaces = new LinkedHashSet<>(8);
433        for (final Row row : rows) {
434            codeSpaces.addAll(row.names.keySet());
435        }
436        return codeSpaces.toArray(new String[codeSpaces.size()]);
437    }
438
439    /**
440     * Returns a HTML anchor for the given category.
441     */
442    private String toAnchor(final String category) {
443        return category.toLowerCase(getLocale()).replace(' ', '-');
444    }
445
446    /**
447     * Formats the current content of the {@linkplain #rows} list as a HTML page in the given file.
448     *
449     * @param  destination  the file to generate.
450     * @return the given {@code destination} file.
451     * @throws IOException if an error occurred while writing the HTML page.
452     */
453    @Override
454    public File write(File destination) throws IOException {
455        Collections.sort(rows);
456        destination = toFile(destination);
457        filter("OperationParameters.html", destination);
458        return destination;
459    }
460
461    /**
462     * Invoked by {@link Report} every time a {@code ${FOO}} occurrence is found.
463     * If the given key is one of those that are managed by this {@code OperationParametersReport}
464     * class, then this method will dispatch to the appropriate {@code writeFoo} method.
465     */
466    @Override
467    final void writeContent(final BufferedWriter out, final String key) throws IOException {
468        if ("CONTENT".equals(key)) {
469            indentation = 6;
470            writeCategories(out);
471            writeTable(out);
472        } else {
473            super.writeContent(out, key);
474        }
475    }
476
477    /**
478     * Writes the list of content before the table of operations. This list is created
479     * only if {@link #getCategory(IdentifiedObject)} returned a non-null value for at
480     * least one operation.
481     *
482     * @param  out  where to write the content.
483     * @throws IOException if an error occurred while writing the content.
484     */
485    private void writeCategories(final BufferedWriter out) throws IOException {
486        String previous = null;
487        for (final Row row : rows) {
488            final String category = row.category;
489            if (category != null && !category.equals(previous)) {
490                if (previous == null) {
491                    writeIndentation(out, indentation); out.write("<p>Content:</p>");
492                    writeIndentation(out, indentation); out.write("<ul>");
493                    out.newLine();
494                    indentation += INDENT;
495                }
496                writeIndentation(out, indentation);
497                out.write("<li><a href=\"#");
498                out.write(toAnchor(category));
499                out.write("\">");
500                out.write(category);
501                out.write("</a></li>\n");
502                previous = category;
503            }
504        }
505        if (previous != null) {
506            indentation -= INDENT;
507            writeIndentation(out, indentation);
508            out.write("</ul>");
509            out.newLine();
510        }
511    }
512
513    /**
514     * Writes the table of operations and their parameters.
515     *
516     * @param  out  where to write the content.
517     * @throws IOException if an error occurred while writing the content.
518     */
519    private void writeTable(final BufferedWriter out) throws IOException {
520        writeIndentation(out, indentation);
521        out.write("<table cellspacing=\"0\" cellpadding=\"0\">");
522        out.newLine();
523        indentation += INDENT;
524        String previous = null;
525        boolean writeHeader = true;
526        final String[] codeSpaces = getColumnHeaders();
527        final String columnSpan = String.valueOf(codeSpaces.length);
528        for (final Row row : rows) {
529            final String category = row.category;
530            /*
531             * If beginning a new section in the table, print the category
532             * in bold characters. The column headers will be printed below.
533             */
534            if (category != null && !category.equals(previous)) {
535                writeIndentation(out, indentation);
536                out.write("<tr class=\"sectionHead\"><th colspan=\"");
537                out.write(columnSpan);         out.write("\" id name=\"");
538                out.write(toAnchor(category)); out.write("\">");
539                out.write(category);           out.write("</th></tr>");
540                out.newLine();
541                writeHeader = true;
542                previous = category;
543            }
544            /*
545             * If printing the first row, or if the above block printed a new category, print
546             * the column headers. Otherwise we will just insert a horizontal separator.
547             */
548            if (writeHeader) {
549                writeIndentation(out, indentation);
550                out.write("<tr class=\"sectionTail\">");
551                for (final String cs : codeSpaces) {
552                    out.write("<th>");
553                    out.write(cs);
554                    out.write("</th>");
555                }
556                out.write("</tr>");
557                out.newLine();
558            }
559            /*
560             * Print the operation name, then the name of all parameters.
561             */
562            writeIndentation(out, indentation);
563            row.write(out, codeSpaces, true, false, false);
564            out.newLine();
565            final List<Row> parameters = row.parameters;
566            if (parameters != null) {
567                indentation += INDENT;
568                final int size = parameters.size();
569                for (int i=0; i<size; i++) {
570                    writeIndentation(out, indentation);
571                    parameters.get(i).write(out, codeSpaces, false, (i == 0), (i == size-1));
572                    out.newLine();
573                }
574                indentation -= INDENT;
575            }
576            writeHeader = false;
577        }
578        indentation -= INDENT;
579        writeIndentation(out, indentation);
580        out.write("</table>");
581    }
582}