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.util.List; 035import java.util.ArrayList; 036import java.util.Map; 037import java.util.HashMap; 038import java.util.LinkedHashMap; 039import java.util.Properties; 040import java.io.File; 041import java.io.IOException; 042import java.io.BufferedWriter; 043 044import org.opengis.util.Factory; 045import org.opengis.util.FactoryException; 046import org.opengis.referencing.crs.CRSAuthorityFactory; 047import org.opengis.referencing.operation.MathTransformFactory; 048 049 050/** 051 * A single point for generating every reports implemented in this package. 052 * Usage example: 053 * 054 * <blockquote><pre> Properties props = new Properties(); 055 * props.setProperty("PRODUCT.NAME", "MyProduct"); 056 * props.setProperty("PRODUCT.URL", "http://www.myproject.org"); 057 * Reports reports = new Reports(props); 058 * reports.addAll(MathTransformFactory.class); 059 * reports.write(new File("my-output-directory"));</pre></blockquote> 060 * 061 * @author Martin Desruisseaux (Geomatys) 062 * @version 3.1 063 * 064 * @since 3.1 065 */ 066public class Reports extends Report { 067 /** 068 * The report generators. 069 */ 070 private final List<Report> reports; 071 072 /** 073 * Instances automatically generated by {@link #add(Factory)}. 074 * Those instances are remembered in order to append new factories to them. 075 */ 076 private final Map<Class<? extends Report>, Report> instances; 077 078 /** 079 * The table of contents, as (title, URL) pairs. 080 */ 081 private final Map<String,File> contents; 082 083 /** 084 * Creates a new report generator using the given property values. 085 * See the {@link Report} Javadoc for a list of expected values. 086 * 087 * @param properties the property values, or {@code null} for the default values. 088 */ 089 public Reports(final Properties properties) { 090 super(properties); 091 reports = new ArrayList<>(); 092 instances = new HashMap<>(); 093 contents = new LinkedHashMap<>(); 094 } 095 096 /** 097 * Adds every kind of report applicable to the given factory. The kind of reports will be 098 * determined from the type of the provided factory. The current implementation can handle 099 * the following kind of factories: 100 * 101 * <ul> 102 * <li>{@link CRSAuthorityFactory}, given to {@link AuthorityCodesReport}</li> 103 * <li>{@link MathTransformFactory}, given to {@link OperationParametersReport}</li> 104 * </ul> 105 * 106 * @param factory the factory for which to generate a report. 107 * @param type the factory type, usually {@code factory.getClass()}. 108 * @return {@code true} if this method will generate a report for the given factory, 109 * or {@code false} if the factory has been ignored. 110 * @throws FactoryException if an error occurred while querying the factory. 111 */ 112 public boolean add(final Factory factory, final Class<? extends Factory> type) throws FactoryException { 113 if (!type.isInstance(factory)) { 114 throw new ClassCastException("Factory of class " + factory.getClass().getCanonicalName() + 115 " is not a kind of " + type.getSimpleName()); 116 } 117 boolean modified = false; 118 if (CRSAuthorityFactory.class.isAssignableFrom(type)) { 119 final AuthorityCodesReport report = getReport(AuthorityCodesReport.class); 120 if (report != null) { 121 report.add((CRSAuthorityFactory) factory); 122 modified = true; 123 } 124 } 125 if (MathTransformFactory.class.isAssignableFrom(type)) { 126 final OperationParametersReport report = getReport(OperationParametersReport.class); 127 if (report != null) { 128 report.add((MathTransformFactory) factory); 129 modified = true; 130 } 131 } 132 return modified; 133 } 134 135 /** 136 * Adds every kinds of report applicable to every factories of the given class found on 137 * the classpath. This method scans the classpath for factories in the way documented 138 * in the {@link org.opengis.test.TestCase#factories(Class[])} method. For each instance 139 * found, {@link #add(Factory, Class)} is invoked. 140 * 141 * @param type the kind of factories to add. 142 * @return {@code true} if this method will generate at least one report for the factories 143 * of the given type, or {@code false} otherwise. 144 * @throws FactoryException if an error occurred while querying the factories. 145 */ 146 public boolean addAll(final Class<? extends Factory> type) throws FactoryException { 147 boolean modified = false; 148 for (final Factory factory : FactoryProvider.forType(type)) { 149 modified |= add(factory, type); 150 } 151 return modified; 152 } 153 154 /** 155 * Adds every kinds of report applicable to every factories of known class found on 156 * the classpath. This method scans the classpath for factories in the way documented 157 * in the {@link org.opengis.test.TestCase#factories(Class[])} method. For each instance 158 * found, {@link #add(Factory, Class)} is invoked. 159 * 160 * @return {@code true} if this method will generate at least one report, or {@code false} otherwise. 161 * @throws FactoryException if an error occurred while querying the factories. 162 */ 163 public boolean addAll() throws FactoryException { 164 return addAll(CRSAuthorityFactory.class) | 165 addAll(MathTransformFactory.class); 166 } 167 168 /** 169 * Returns a report of the given type. If a report has already been created by a previous 170 * invocation of this method. Then that report is returned. Otherwise a new report is 171 * created and cached for appending. 172 * 173 * @param <T> the compile-time type of the {@code type} argument. 174 * @param type the kind of report to create. 175 * @return the report of the given type, or {@code null} if no report of the given type should be generated. 176 * @throws IllegalArgumentException if the given type is not a report that can be instantiated. 177 */ 178 private <T extends Report> T getReport(final Class<T> type) throws IllegalArgumentException { 179 final Report candidate = instances.get(type); 180 if (candidate != null) { 181 return type.cast(candidate); 182 } 183 final T report = createReport(type); 184 if (report != null) { 185 if (reports.isEmpty()) { 186 /* 187 * If we are creating the first report, creates the initial listener which will 188 * delegate the calls to 'ProgressListener.progress(int,int)' to the 'progress' 189 * method in this class. All other listeners will be chained before this one. 190 * 191 * We need to wrap the "delegator" listener into an other listener in order to 192 * allow all future listeners to be inserted between the two: the "delegator" 193 * listener must always be last, and the listener associated to the first report 194 * must stay first. 195 */ 196 report.listener = new ProgressListener(new ProgressListener(null, false) { 197 @Override void progress(final int position, final int count) { 198 Reports.this.progress(position, count); 199 } 200 }, false); 201 } else { 202 report.listener = new ProgressListener(reports.get(0).listener, true); 203 } 204 instances.put(type, report); 205 reports.add(report); 206 } 207 return report; 208 } 209 210 /** 211 * Invoked when {@code Reports} need to create a new instance of the given class. 212 * Subclasses can override this method in order to customize their {@code Report} 213 * instances. 214 * 215 * <p>The default implementation creates a new instance of the given classes using 216 * the {@linkplain java.lang.reflect.Constructor#newInstance(Object[]) reflection API}. 217 * The given type shall declare a public constructor expecting a single {@link Properties} 218 * argument.</p> 219 * 220 * @param <T> the compile-time type of the {@code type} argument. 221 * @param type the kind of report to create. 222 * @return the report of the given type, or {@code null} if no report of the given type should be generated. 223 * @throws IllegalArgumentException if the given type is not a kind of report that this method can instantiate. 224 */ 225 protected <T extends Report> T createReport(final Class<T> type) throws IllegalArgumentException { 226 try { 227 return type.cast(type.getConstructor(Properties.class).newInstance(properties)); 228 } catch (ReflectiveOperationException e) { 229 throw new IllegalArgumentException("Can not instantiate report of type " + type.getSimpleName(), e); 230 } 231 } 232 233 /** 234 * Writes in the given directory every reports {@linkplain #add(Factory, Class) added} 235 * to this {@code Reports} instance. 236 * 237 * @param directory the directory where to write the reports. 238 * @return the index file, or the main file in only one report has been created, 239 * or {@code null} if no report has been created. 240 * @throws IOException if an error occurred while writing a report. 241 */ 242 @Override 243 public File write(final File directory) throws IOException { 244 File main = null; 245 boolean needsTOC = false; 246 for (final Report report : reports) { 247 File file = report.toFile(directory); 248 file = report.write(file); 249 final String title = report.getProperty("TITLE").trim(); 250 if (!title.isEmpty()) { 251 if (contents.put(title, relativize(directory, file)) != null) { 252 throw new IOException("Duplicated title: " + title); 253 } 254 } 255 if (file != null) { 256 if (main == null) { 257 main = file; 258 } else if (!needsTOC) { 259 main = new File(directory, "index.html"); 260 needsTOC = true; 261 } 262 } 263 } 264 if (needsTOC) { 265 filter("index.html", main); 266 } 267 return main; 268 } 269 270 /** 271 * Invoked by {@link Report} every time a {@code ${FOO}} occurrence is found. 272 * This method generates the table of content. 273 */ 274 @Override 275 final void writeContent(final BufferedWriter out, final String key) throws IOException { 276 if (!"CONTENT".equals(key)) { 277 super.writeContent(out, key); 278 return; 279 } 280 for (final Map.Entry<String,File> entry : contents.entrySet()) { 281 writeIndentation(out, 8); 282 out.write("<li><a href=\""); 283 out.write(escape(entry.getValue().getPath().replace(File.separatorChar, '/'))); 284 out.write("\">"); 285 out.write(entry.getKey()); // Already escaped. 286 out.write("</a></li>"); 287 out.newLine(); 288 } 289 } 290 291 /** 292 * Returns a file relative to the given directory. 293 */ 294 private static File relativize(final File directory, File file) { 295 File relative = new File(file.getName()); 296 while ((file = file.getParentFile()) != null) { 297 if (file.equals(directory)) { 298 break; 299 } 300 relative = new File(file.getName(), relative.getPath()); 301 } 302 return relative; 303 } 304}