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}