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