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