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}