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