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"> </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"> </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"> </td> <td>Name of the product for which the report is generated.</td></tr> 074 * <tr><td>{@code PRODUCT.VERSION}</td><td align="center"> </td> <td>Version of the product for which the report is generated.</td></tr> 075 * <tr><td>{@code PRODUCT.URL}</td> <td align="center"> </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}