View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one   *
3    * or more contributor license agreements.  See the NOTICE file *
4    * distributed with this work for additional information        *
5    * regarding copyright ownership.  The ASF licenses this file   *
6    * to you under the Apache License, Version 2.0 (the            *
7    * "License"); you may not use this file except in compliance   *
8    * with the License.  You may obtain a copy of the License at   *
9    *                                                              *
10   *   http://www.apache.org/licenses/LICENSE-2.0                 *
11   *                                                              *
12   * Unless required by applicable law or agreed to in writing,   *
13   * software distributed under the License is distributed on an  *
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
15   * KIND, either express or implied.  See the License for the    *
16   * specific language governing permissions and limitations      *
17   * under the License.                                           *
18   */
19  package org.apache.rat;
20  
21  import java.io.File;
22  import java.io.FileFilter;
23  import java.io.FileInputStream;
24  import java.io.FileOutputStream;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.OutputStream;
28  import java.io.OutputStreamWriter;
29  import java.io.PrintWriter;
30  import java.net.MalformedURLException;
31  import java.net.URI;
32  import java.net.URL;
33  import java.nio.charset.StandardCharsets;
34  import java.nio.file.Files;
35  import java.util.ArrayList;
36  import java.util.Collection;
37  import java.util.List;
38  import java.util.Map;
39  import java.util.Objects;
40  import java.util.SortedSet;
41  import java.util.function.Consumer;
42  import java.util.stream.Stream;
43  
44  import org.apache.commons.collections4.set.UnmodifiableSortedSet;
45  import org.apache.commons.io.function.IOSupplier;
46  import org.apache.commons.io.output.CloseShieldOutputStream;
47  import org.apache.commons.lang3.StringUtils;
48  import org.apache.rat.analysis.IHeaderMatcher;
49  import org.apache.rat.api.RatException;
50  import org.apache.rat.commandline.StyleSheets;
51  import org.apache.rat.config.AddLicenseHeaders;
52  import org.apache.rat.config.exclusion.ExclusionProcessor;
53  import org.apache.rat.config.exclusion.StandardCollection;
54  import org.apache.rat.config.results.ClaimValidator;
55  import org.apache.rat.configuration.XMLConfigurationReader;
56  import org.apache.rat.configuration.builders.AnyBuilder;
57  import org.apache.rat.document.DocumentName;
58  import org.apache.rat.document.DocumentNameMatcher;
59  import org.apache.rat.document.FileDocument;
60  import org.apache.rat.license.ILicense;
61  import org.apache.rat.license.ILicenseFamily;
62  import org.apache.rat.license.LicenseSetFactory;
63  import org.apache.rat.license.LicenseSetFactory.LicenseFilter;
64  import org.apache.rat.report.RatReport;
65  import org.apache.rat.report.Reportable;
66  import org.apache.rat.report.claim.ClaimStatistic;
67  import org.apache.rat.report.xml.writer.XmlWriter;
68  import org.apache.rat.utils.DefaultLog;
69  import org.apache.rat.utils.Log.Level;
70  import org.apache.rat.utils.ReportingSet;
71  import org.apache.rat.utils.StandardXmlFactory;
72  import org.apache.rat.walker.FileListWalker;
73  import org.apache.rat.walker.ReportableListWalker;
74  import org.w3c.dom.Node;
75  import org.xml.sax.SAXException;
76  
77  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
78  
79  /**
80   * A configuration object is used by the front end to invoke the
81   * {@link Reporter}. The sole purpose of the frontends is to create the
82   * configuration and invoke the {@link Reporter}.
83   */
84  public class ReportConfiguration {
85  
86      /** The IODescriptor for {@code System.out}. */
87      public static final IODescriptor<OutputStream> SYSTEM_OUT =
88              // SONAR wants to require logging output, which is the wrong reporting channel for this case.
89              new IODescriptor<>("System.out", () -> CloseShieldOutputStream.wrap(System.out)); // NOSONAR
90  
91      /**
92       * The styles of processing for various categories of documents.
93       */
94      public enum Processing {
95          /** List file as present only. */
96          NOTIFICATION("List file as present"),
97          /** List all present licenses. */
98          PRESENCE("List any licenses found"),
99          /** List all present licenses and unknown licenses. */
100         ABSENCE("List licenses found and any unknown licences");
101 
102         /**
103          * Description of the processing.
104          */
105         private final String description;
106 
107         Processing(final String description) {
108             this.description = description;
109         }
110 
111         /**
112          * Gets the description of the processing type.
113          * @return the description of the processing type.
114          */
115         public String desc() {
116             return description;
117         }
118     }
119 
120     /** The LicenseSetFactory for the configuration */
121     private final LicenseSetFactory licenseSetFactory;
122 
123     /**
124      * {@code true} if we are adding license headers to the files.
125      */
126     private boolean addingLicenses;
127 
128     /**
129      * {@code true} if we are adding license headers in place (no *.new files)
130      */
131     private boolean addingLicensesForced;
132 
133     /**
134      * The copyright message to add if we are adding headers. Will be {@code null}
135      * if we are not adding copyright messages.
136      */
137     private String copyrightMessage;
138 
139     /**
140      * The IODescriptor that provides the output stream to write the report to.
141      */
142     private IODescriptor<OutputStream> out;
143 
144     /**
145      * The IODescriptor that provides the stylesheet to style the XML output.
146      */
147     private IODescriptor<InputStream> styleSheet;
148 
149     /**
150      * A list of files to read file names from.
151      */
152     private final List<File> sources;
153 
154     /**
155      * A list of reportables to process.
156      */
157     private final List<Reportable> reportables;
158 
159     /**
160      * The exclusion processor that determines if a file is included or excluded.
161      */
162     private final ExclusionProcessor exclusionProcessor;
163 
164     /**
165      * The default filter for displaying families.
166      */
167     private LicenseFilter listFamilies;
168 
169     /**
170      * The default filter for displaying licenses.
171      */
172     private LicenseFilter listLicenses;
173 
174     /**
175      * {@code true} if this is a dry run and no processing is to take place.
176      */
177     private boolean dryRun;
178 
179     /**
180      * How to process ARCHIVE document types.
181      */
182     private Processing archiveProcessing;
183 
184     /**
185      * How to process STANDARD document types.
186      */
187     private Processing standardProcessing;
188 
189     /**
190      * The ClaimValidator to validate min/max counts and similar claims.
191      */
192     private final ClaimValidator claimValidator;
193 
194     /**
195      * Constructor
196      */
197     public ReportConfiguration() {
198         licenseSetFactory = new LicenseSetFactory();
199         listFamilies = Defaults.LIST_FAMILIES;
200         listLicenses = Defaults.LIST_LICENSES;
201         dryRun = false;
202         exclusionProcessor = new ExclusionProcessor();
203         claimValidator = new ClaimValidator();
204         sources = new ArrayList<>();
205         reportables = new ArrayList<>();
206     }
207 
208     public SerDes serDes() {
209         return new SerDes();
210     }
211 
212     /**
213      * Report the excluded files to the appendable object.
214      * @param appendable the appendable object to write to.
215      */
216     public void reportExclusions(final Appendable appendable) {
217         try {
218             exclusionProcessor.reportExclusions(appendable);
219         } catch (IOException e) {
220             DefaultLog.getInstance().warn("Unable to report exclusions", e);
221         }
222     }
223 
224     /**
225      * Adds a file as a source of files to scan.
226      * The file must be a text file that lists files to be included.
227      * File within the file must be in linux format with a
228      * {@code "/"} file separator.
229      * @param file the file to process.
230      */
231     public void addSource(final File file) {
232         notNull(file, "File may not be null.");
233         sources.add(file);
234     }
235 
236     private void notNull(final Object o, final String msg) {
237         if (o == null) {
238             throw new ConfigurationException(msg);
239         }
240     }
241 
242     /**
243      * Adds a Reportable as a source of files to scan.
244      * @param reportable the reportable to process.
245      */
246     public void addSource(final Reportable reportable) {
247         notNull(reportable, "Reportable may not be null.");
248         reportables.add(reportable);
249     }
250 
251     /**
252      * Returns {@code true} if the configuration has any sources defined.
253      * @return {@code true} if the configuration has any sources defined.
254      */
255     public boolean hasSource() {
256         return !reportables.isEmpty() || !sources.isEmpty();
257     }
258 
259     /**
260      * Gets a builder initialized with any files specified as sources.
261      * @return a configured builder.
262      */
263     public ReportableListWalker.Builder getSources() {
264         DocumentName name = DocumentName.builder(new File(".")).build();
265         ReportableListWalker.Builder builder = ReportableListWalker.builder(name);
266         sources.forEach(file -> builder.addReportable(new FileListWalker(new FileDocument(file, DocumentNameMatcher.MATCHES_ALL))));
267         reportables.forEach(builder::addReportable);
268         return builder;
269     }
270 
271     // for testing access
272     Iterable<File> sources() {
273         return sources;
274     }
275 
276     // for testing access
277     Stream<DocumentName> reportables() {
278         return reportables.stream().map(Reportable::name);
279     }
280 
281     /**
282      * Gets the matcher that matches generated text.
283      * @return the matcher that matches generated text.
284      */
285     public IHeaderMatcher getGeneratedMatcher() {
286         return new AnyBuilder().setResource("/org/apache/rat/generation-keywords.txt").build();
287     }
288 
289     /**
290      * Retrieves the archive processing type.
291      * @return the archive processing type.
292      */
293     public Processing getArchiveProcessing() {
294         return archiveProcessing == null ? Defaults.ARCHIVE_PROCESSING : archiveProcessing;
295     }
296 
297     /**
298      * Sets the archive processing type. If not set will default to NOTIFICATION.
299      * @param archiveProcessing the type of processing archives should have.
300      */
301     public void setArchiveProcessing(final Processing archiveProcessing) {
302         this.archiveProcessing = archiveProcessing;
303     }
304 
305     /**
306      * Retrieves the standard processing type.
307      * @return the standard processing type.
308      */
309     public Processing getStandardProcessing() {
310         return standardProcessing == null ? Defaults.STANDARD_PROCESSING : standardProcessing;
311     }
312 
313     /**
314      * Sets the standard processing type. If not set will default to NOTIFICATION.
315      * @param standardProcessing the type of processing standard files should have.
316      */
317     public void setStandardProcessing(final Processing standardProcessing) {
318         this.standardProcessing = standardProcessing;
319     }
320 
321     /**
322      * Set the log level for reporting collisions in the set of license families.
323      * <p>NOTE: should be set before licenses or license families are added.</p>
324      * @param level the log level to use.
325      */
326     public void logFamilyCollisions(final Level level) {
327         licenseSetFactory.logFamilyCollisions(level);
328     }
329 
330     /**
331      * Sets the reporting option for duplicate license families.
332      * @param state the ReportingSet.Option to use for reporting.
333      */
334     public void familyDuplicateOption(final ReportingSet.Options state) {
335         licenseSetFactory.familyDuplicateOption(state);
336     }
337 
338     /**
339      * Sets the log level for reporting license collisions.
340      * @param level the log level.
341      */
342     public void logLicenseCollisions(final Level level) {
343         licenseSetFactory.logLicenseCollisions(level);
344     }
345 
346     /**
347      * Sets the reporting option for duplicate licenses.
348      * @param state the ReportingSet.Option to use for reporting.
349      */
350     public void licenseDuplicateOption(final ReportingSet.Options state) {
351         licenseSetFactory.licenseDuplicateOption(state);
352     }
353 
354     /**
355      * Set the level of license families that should be output in the XML document.
356      * @param filter the license families to list.
357      */
358     public void listFamilies(final LicenseFilter filter) {
359         listFamilies = filter;
360     }
361 
362     /**
363      * Return the current filter that determines which families will be output in the XML document.
364      * @return the filter that defines the families to list.
365      */
366     public LicenseFilter listFamilies() {
367         return listFamilies;
368     }
369 
370     /**
371      * Set the level of licenses that should be output in the XML document.
372      * @param filter the licenses to list.
373      */
374     public void listLicenses(final LicenseFilter filter) {
375         listLicenses = filter;
376     }
377 
378     /**
379      * Gets the selected license filter.
380      * @return the filter to limit license display.
381      */
382     public LicenseFilter listLicenses() {
383         return listLicenses;
384     }
385 
386     /**
387      * Sets the dry run flag.
388      * @param state the state for the dry run flag.
389      */
390     public void setDryRun(final boolean state) {
391         dryRun = state;
392     }
393 
394     /**
395      * Returns the state of the dry run flag.
396      * @return the state of the dry run flag.
397      */
398     public boolean isDryRun() {
399         return dryRun;
400     }
401 
402     /**
403      * Excludes a StandardCollection of patterns.
404      * @param collection the StandardCollection to exclude.
405      * @see ExclusionProcessor#addExcludedCollection(StandardCollection)
406      */
407     public void addExcludedCollection(final StandardCollection collection) {
408         exclusionProcessor.addExcludedCollection(collection);
409     }
410 
411     /**
412      * Excludes the file processor defined in the StandardCollection.
413      * @param collection the StandardCollection to exclude.
414      * @see ExclusionProcessor#addFileProcessor(StandardCollection)
415      */
416     public void addExcludedFileProcessor(final StandardCollection collection) {
417         exclusionProcessor.addFileProcessor(collection);
418     }
419 
420     /**
421      * Excludes files that match a FileFilter.
422      * @param fileFilter the file filter to match.
423      */
424     public void addExcludedFilter(final FileFilter fileFilter) {
425         exclusionProcessor.addExcludedMatcher(new DocumentNameMatcher(fileFilter));
426     }
427 
428     /**
429      * Excludes files that match a DocumentNameMatcher.
430      * @param matcher the DocumentNameMatcher to match.
431      */
432     public void addExcludedMatcher(final DocumentNameMatcher matcher) {
433         exclusionProcessor.addExcludedMatcher(matcher);
434     }
435 
436     /**
437      * Excludes files that match the pattern.
438      *
439      * @param patterns the collection of patterns to exclude.
440      * @see ExclusionProcessor#addIncludedPatterns(Iterable)
441      */
442     public void addExcludedPatterns(final Iterable<String> patterns) {
443         exclusionProcessor.addExcludedPatterns(patterns);
444     }
445 
446     /**
447      * Adds the patterns from the standard collection as included patterns.
448      * @param collection the standard collection to include.
449      */
450     public void addIncludedCollection(final StandardCollection collection) {
451         exclusionProcessor.addIncludedCollection(collection);
452     }
453 
454     /**
455      * Adds the fileFilter to filter files that should be included, this overrides any
456      * exclusion of the same files.
457      * @param fileFilter the filter to identify files that should be included.
458      */
459     public void addIncludedFilter(final FileFilter fileFilter) {
460         exclusionProcessor.addIncludedMatcher(new DocumentNameMatcher(fileFilter));
461     }
462 
463     /**
464      * Includes files that match a DocumentNameMatcher.
465      * @param matcher the DocumentNameMatcher to match.
466      */
467     public void addIncludedMatcher(final DocumentNameMatcher matcher) {
468         exclusionProcessor.addIncludedMatcher(matcher);
469     }
470 
471     /**
472      * Add file patterns that are to be included. These patterns override any exclusion of
473      * the same files.
474      * @param patterns the iterable of Strings containing the patterns.
475      */
476     public void addIncludedPatterns(final Iterable<String> patterns) {
477         exclusionProcessor.addIncludedPatterns(patterns);
478     }
479 
480     /**
481      * Get the DocumentNameMatcher that excludes files found in the directory tree.
482      * @param baseDir the DocumentName for the base directory.
483      * @return the DocumentNameMatcher for the base directory.
484      */
485     public DocumentNameMatcher getDocumentExcluder(final DocumentName baseDir) {
486         return exclusionProcessor.getNameMatcher(baseDir);
487     }
488 
489     // visible for testing.
490     ExclusionProcessor getExclusionProcessor() {
491         return exclusionProcessor;
492     }
493 
494     /**
495      * Gets the IOSupplier with the style sheet.
496      * @return the Supplier of the InputStream that is the XSLT style sheet to style
497      * the report with.
498      */
499     public IOSupplier<InputStream> getStyleSheet() {
500         return styleSheet == null ? null : styleSheet.ioSupplier();
501     }
502 
503     /**
504      * Gets the IODescriptor with the style sheet.
505      * @return the IODescriptor that describes the XSLT style sheet to style
506      * the report with.
507      */
508     public IODescriptor<InputStream> getStyleSheetDescriptor() {
509         return styleSheet == null ? null : styleSheet;
510     }
511 
512     /**
513      * Sets the style sheet for custom processing. The IODescriptor may be called
514      * multiple times, so the input stream must be able to be opened and closed
515      * multiple times.
516      * @param styleSheet the XSLT style sheet to style the report with.
517      */
518     public void setStyleSheet(final IODescriptor<InputStream> styleSheet) {
519         this.styleSheet = styleSheet;
520     }
521 
522     /**
523      * Adds the licenses and approved licenses from the defaults object to the
524      * configuration. <em>Side effect:</em> if the report should be styled and no
525      * style sheet has been set the plain stylesheet from the defaults will be used.
526      * @param defaults the defaults to set.
527      */
528     public void setFrom(final Defaults defaults) {
529         licenseSetFactory.add(defaults.getLicenseSetFactory());
530         if (getStyleSheet() == null) {
531             setStyleSheet(StyleSheets.PLAIN.getStyleSheet());
532         }
533         defaults.getStandardExclusion().forEach(this::addExcludedCollection);
534     }
535 
536     /**
537      * Sets the style sheet.
538      * @param styleSheet the XSLT style sheet file to style the report with.
539      */
540     public void setStyleSheet(final File styleSheet) {
541         Objects.requireNonNull(styleSheet, "styleSheet file should not be null");
542         setStyleSheet(styleSheet.toURI());
543     }
544 
545     /**
546      * Sets the style sheet for custom processing. The stylesheet may be opened
547      * multiple times so the URI must be capable of being opened multiple times.
548      * @param styleSheet the URI of the XSLT style sheet to style the report with.
549      */
550     public void setStyleSheet(final URI styleSheet) {
551         Objects.requireNonNull(styleSheet, "Stylesheet file must not be null");
552         try {
553             setStyleSheet(styleSheet.toURL());
554         } catch (MalformedURLException e) {
555             throw new ConfigurationException("Unable to process stylesheet", e);
556         }
557     }
558 
559     /**
560      * Sets the style sheet for custom processing. The stylesheet may be opened
561      * multiple times so the URL must be capable of being opened multiple times.
562      * @param styleSheet the URL of the XSLT style sheet to style the report with.
563      */
564     public void setStyleSheet(final URL styleSheet) {
565         Objects.requireNonNull(styleSheet, "Stylesheet file must not be null");
566         setStyleSheet(new IODescriptor<>(styleSheet.toString(), styleSheet::openStream));
567     }
568 
569     /**
570      * Sets the supplier for the output stream. The supplier may be called multiple
571      * times to provide the stream. Suppliers should prepare streams that are
572      * appended to and that can be closed. If an {@code OutputStream} should not be
573      * closed consider wrapping it in a {@code CloseShieldOutputStream}
574      * @param out the OutputStream supplier that provides the output stream to write
575      * the report to. A {@code null} value will use {@code System.out}.
576      * @see CloseShieldOutputStream
577      */
578     public void setOut(final IODescriptor<OutputStream> out) {
579         this.out = out;
580     }
581 
582     /**
583      * Sets the OutputStream supplier to use the specified file. The file may be
584      * opened and closed several times. File is deleted first and then may be
585      * repeatedly opened in append mode.
586      * @see #setOut(IODescriptor)
587      * @param file The file to create the supplier with.
588      */
589     public void setOut(final File file) {
590         Objects.requireNonNull(file, "output file should not be null");
591         if (file.exists()) {
592             try {
593                 Files.delete(file.toPath());
594             } catch (IOException e) {
595                 DefaultLog.getInstance().warn("Unable to delete file: " + file);
596             }
597         }
598 
599         File parent = file.getParentFile();
600         if (!parent.mkdirs() && !parent.isDirectory()) {
601             DefaultLog.getInstance().warn("Unable to create directory: " + file.getParentFile());
602         }
603         setOut(IODescriptor.output(file));
604     }
605 
606     /**
607      * Returns the output stream supplier. If no stream has been set returns a
608      * supplier for {@code System.out}.
609      * @return the supplier of the output stream to write the report to.
610      */
611     public IOSupplier<OutputStream> getOutput() {
612         return getOutputDescriptor().ioSupplier();
613     }
614 
615     /**
616      * Returns the output IODescriptor. If no stream has been set returns a
617      * descriptor for {@code System.out}.
618      * @return the IODescriptor of the output stream to write the report to.
619      */
620     public IODescriptor<OutputStream> getOutputDescriptor() {
621         return out == null ? SYSTEM_OUT : out;
622     }
623 
624     /**
625      * Gets a PrintWriter that wraps the output stream.
626      * @return a supplier for a PrintWriter that wraps the output stream.
627      * @see #getOutput()
628      */
629     public IOSupplier<PrintWriter> getWriter() {
630         return () -> new PrintWriter(new OutputStreamWriter(getOutput().get(), StandardCharsets.UTF_8));
631     }
632 
633     /**
634      * Adds a license to the list of licenses. Does not add the license to the list
635      * of approved licenses.
636      * @param license the license to add to the list of licenses.
637      */
638     public void addLicense(final ILicense license) {
639         licenseSetFactory.addLicense(license);
640     }
641 
642     /**
643      * Adds a license to the list of licenses. Does not add the license to the list
644      * of approved licenses.
645      * @param builder the license builder to build and add to the list of licenses.
646      * @return the ILicense implementation that was added.
647      */
648     public ILicense addLicense(final ILicense.Builder builder) {
649         return licenseSetFactory.addLicense(builder);
650     }
651 
652     /**
653      * Adds multiple licenses to the list of licenses. Does not add the licenses to
654      * the list of approved licenses.
655      * @param licenses the licenses to add.
656      */
657     public void addLicenses(final Collection<ILicense> licenses) {
658         licenseSetFactory.addLicenses(licenses);
659     }
660 
661     /**
662      * Adds a license family to the list of families. Does not add the family to the
663      * list of approved licenses.
664      * @param family the license family to add to the list of license families.
665      */
666     public void addFamily(final ILicenseFamily family) {
667        licenseSetFactory.addFamily(family);
668     }
669 
670     /**
671      * Adds a license family to the list of families. Does not add the family to the
672      * list of approved licenses.
673      * @param builder the licenseFamily.Builder to build and add to the list of
674      * licenses.
675      */
676     public void addFamily(final ILicenseFamily.Builder builder) {
677         licenseSetFactory.addFamily(builder);
678     }
679 
680     /**
681      * Adds multiple families to the list of license families. Does not add the
682      * licenses to the list of approved licenses.
683      * @param families the license families to add.
684      */
685     public void addFamilies(final Collection<ILicenseFamily> families) {
686         families.forEach(this::addApprovedLicenseCategory);
687     }
688 
689     /**
690      * Adds an ILicenseFamily to the list of approved licenses.
691      * @param approvedILicenseFamily the LicenseFamily to add.
692      */
693     public void addApprovedLicenseCategory(final ILicenseFamily approvedILicenseFamily) {
694         addApprovedLicenseCategory(approvedILicenseFamily.getFamilyCategory());
695     }
696 
697     /**
698      * Adds a license family category (id) to the list of approved licenses.
699      * @param familyCategory the category to add.
700      */
701     public void addApprovedLicenseCategory(final String familyCategory) {
702         licenseSetFactory.approveLicenseCategory(familyCategory);
703     }
704 
705     /**
706      * Adds a collection of license family categories to the set of approved license
707      * names.
708      * @param approvedLicenseCategories set of approved license categories.
709      */
710     public void addApprovedLicenseCategories(final Collection<String> approvedLicenseCategories) {
711         approvedLicenseCategories.forEach(this::addApprovedLicenseCategory);
712     }
713 
714     /**
715      * Adds a license family category to the list of approved licenses. <em>Once a
716      * license has been removed from the approved list it cannot be re-added</em>
717      * @param familyCategory the category to add.
718      */
719     public void removeApprovedLicenseCategory(final String familyCategory) {
720         licenseSetFactory.removeLicenseCategory(ILicenseFamily.makeCategory(familyCategory));
721     }
722 
723     /**
724      * Removes a license family category from the list of approved licenses.
725      * <em>Once a license has been removed from the approved list it cannot be
726      * re-added</em>
727      * @param familyCategory the family category to remove.
728      */
729     public void removeApprovedLicenseCategories(final Collection<String> familyCategory) {
730         familyCategory.forEach(this::removeApprovedLicenseCategory);
731     }
732 
733     /**
734      * Gets the SortedSet of approved license categories. <em>Once a license has
735      * been removed from the approved list it cannot be re-added</em>
736      * @param filter the LicenseFilter to filter the categories by.
737      * @return the Sorted set of approved license categories.
738      */
739     public SortedSet<String> getLicenseCategories(final LicenseFilter filter) {
740         return licenseSetFactory.getLicenseCategories(filter);
741     }
742 
743     /**
744      * Gets the SortedSet of approved license categories. <em>Once a license has
745      * been removed from the approved list it cannot be re-added</em>
746      * @param filter the LicenseFilter to filter the licenses by.
747      * @return the Sorted set of approved license categories.
748      */
749     public UnmodifiableSortedSet<ILicense> getLicenses(final LicenseFilter filter) {
750         return licenseSetFactory.getLicenses(filter);
751     }
752 
753     /**
754      * Gets the SortedSet of approved license categories. <em>Once a license has
755      * been removed from the approved list it cannot be re-added</em>
756      * @param filter the LicenseFilter to filter the licenses by.
757      * @return the Sorted set of approved license categories.
758      */
759     public SortedSet<String> getLicenseIds(final LicenseFilter filter) {
760         return licenseSetFactory.getLicenseIds(filter);
761     }
762 
763     /**
764      * Adds an ILicenseFamily to the list of approved licenses.
765      * @param approvedLicense the License to add.
766      */
767     public void addApprovedLicenseId(final ILicense approvedLicense) {
768         addApprovedLicenseId(approvedLicense.getId());
769     }
770 
771     /**
772      * Adds a license family category (id) to the list of approved licenses
773      * @param licenseId the license id to add.
774      */
775     public void addApprovedLicenseId(final String licenseId) {
776         licenseSetFactory.approveLicenseId(licenseId);
777     }
778 
779     /**
780      * Adds a collection of license family categories to the set of approved license
781      * names.
782      * @param approvedLicenseIds set of approved license IDs.
783      */
784     public void addApprovedLicenseIds(final Collection<String> approvedLicenseIds) {
785         approvedLicenseIds.forEach(this::addApprovedLicenseId);
786     }
787 
788     /**
789      * Adds a license family category to the list of approved licenses. <em>Once a
790      * license has been removed from the approved list it cannot be re-added</em>
791      * @param licenseId the license ID to add.
792      */
793     public void removeApprovedLicenseId(final String licenseId) {
794         licenseSetFactory.removeLicenseId(licenseId);
795     }
796 
797     /**
798      * Removes a license family category from the list of approved licenses.
799      * <em>Once a license has been removed from the approved list it cannot be
800      * re-added</em>
801      * @param licenseIds the license IDs to remove.
802      */
803     public void removeApprovedLicenseIds(final Collection<String> licenseIds) {
804         licenseIds.forEach(this::removeApprovedLicenseId);
805     }
806 
807     /**
808      * Returns the optional license copyright being added if RAT is adding headers.
809      * This value is ignored, if no license headers are added.
810      * @return the optional copyright message.
811      * @see #isAddingLicenses()
812      */
813     public String getCopyrightMessage() {
814         return copyrightMessage;
815     }
816 
817     /**
818      * Sets the optional copyright message used if RAT is adding license headers.
819      * This value is ignored, if no license headers are added.
820      * @param copyrightMessage message to set.
821      * @see #isAddingLicenses()
822      */
823     public void setCopyrightMessage(final String copyrightMessage) {
824         this.copyrightMessage = copyrightMessage;
825     }
826 
827     /**
828      * Gets the flag that determines if license headers are "forced" overwriting existing files.
829      * This value is ignored if RAT is not adding licenses.
830      * @return {@code true} if RAT is forcing the adding license headers.
831      * @see #isAddingLicenses()
832      */
833     public boolean isAddingLicensesForced() {
834         return addingLicensesForced;
835     }
836 
837     /**
838      * Gets the flag that determines if license headers should be added if missing.
839      * @return whether RAT should add missing license headers.
840      * @see #isAddingLicensesForced()
841      * @see #getCopyrightMessage()
842      */
843     public boolean isAddingLicenses() {
844         return addingLicenses;
845     }
846 
847     /**
848      * Sets whether RAT should enable, disable, or force the adding of license
849      * headers.
850      * @param addLicenseHeaders enables/disables or forces adding of licenses
851      * headers.
852      * @see #isAddingLicenses()
853      * @see #setCopyrightMessage(String)
854      */
855     public void setAddLicenseHeaders(final AddLicenseHeaders addLicenseHeaders) {
856         addingLicenses = false;
857         addingLicensesForced = false;
858         switch (addLicenseHeaders) {
859         case FALSE:
860             // do nothing
861             break;
862         case FORCED:
863             addingLicensesForced = true;
864             addingLicenses = true;
865             break;
866         case TRUE:
867             addingLicenses = true;
868             break;
869         }
870     }
871 
872     /**
873      * Gets a sorted set of ILicenseFamily objects based on {@code filter}. If
874      * filter is set to:
875      * <ul>
876      * <li>{@code all} - All licenses families will be returned.</li>
877      * <li>{@code approved} - Only approved license families will be returned.</li>
878      * <li>{@code none} - No license families will be returned.</li>
879      * </ul>
880      * @param filter the license filter.
881      * @return the set of defined licenses.
882      */
883     public SortedSet<ILicenseFamily> getLicenseFamilies(final LicenseFilter filter) {
884         return licenseSetFactory.getLicenseFamilies(filter);
885     }
886 
887     /**
888      * Gets the ClaimValidator for the configuration.
889      * @return the ClaimValidator.
890      */
891     public ClaimValidator getClaimValidator() {
892         return claimValidator;
893     }
894 
895     /**
896      * Gets the enclosed LicenseSetFactory.
897      * @return the license set factory.
898      */
899     public LicenseSetFactory getLicenseSetFactory() {
900         return licenseSetFactory;
901     }
902 
903     /**
904      * Validates that the configuration is valid.
905      * @param logger String consumer to log warning messages to.
906      * @throws ConfigurationException on configuration error.
907      */
908     public void validate(final Consumer<String> logger) {
909         if (!hasSource()) {
910             String msg = "At least one source must be specified";
911             logger.accept(msg);
912             throw new ConfigurationException(msg);
913         }
914         if (licenseSetFactory.getLicenses(LicenseFilter.ALL).isEmpty()) {
915             String msg = "You must specify at least one license";
916             logger.accept(msg);
917             throw new ConfigurationException(msg);
918         }
919     }
920 
921     /**
922      * An IODescriptor comprises a name and an IOSupplier. The name should identify the contents of the stream.
923      * @param name the name of the supplier.
924      * @param ioSupplier the IOSupplier that provides either an InputStream or an OutputStream.
925      * @param <T> either InputStream or OutputStream.
926      */
927     public record IODescriptor<T>(String name, IOSupplier<T> ioSupplier) {
928 
929         // OUTPUT CONSTRUCTORS
930         /**
931          * Creates an output IODescriptor for the file name within the working directory.
932          * @param name the name of the file to open.
933          * @param workingDirectory the working directory for the file.
934          * @return the Output IODescriptor.
935          */
936         static IODescriptor<OutputStream> output(final String name, final DocumentName workingDirectory) {
937             DocumentName docName = workingDirectory.resolve(name);
938             return new IODescriptor<>(name, () -> new FileOutputStream(docName.asFile()));
939         }
940 
941         /**
942          * Creates an output IODescriptor for the file. Does not modify for working directory.
943          * @param file the file to open.
944          * @return the Output IODescriptor.
945          */
946         static IODescriptor<OutputStream> output(final File file) {
947             return new IODescriptor<>(file.toString(), () -> new FileOutputStream(file, true));
948         }
949 
950         // INPUT CONSTRUCTORS
951         /**
952          * Creates an input IODescriptor for the file. Does not modify for working directory.
953          * @param file the file to open.
954          * @return the Input IODescriptor.
955          */
956         static IODescriptor<InputStream> input(final File file) {
957             return new IODescriptor<>(file.toString(), () -> new FileInputStream(file));
958         }
959     }
960 
961     /**
962      * Serializes the ReportConfiguration into an XML document that can be deserialized by the Serde.
963      * Deserialized ReportConfigurations can not be executed as the reportable objects use named placeholders
964      * and do not have access to the original object.
965      */
966     @SuppressFBWarnings("EI_EXPOSE_REP2")
967     public class SerDes {
968         /**
969          * Writes the configuration as an XML document to the appendable.
970          *
971          * @param appendable the Appendable to write to.
972          * @throws IOException on error.
973          */
974         public void serialize(final Appendable appendable) throws IOException {
975             try (XmlWriter writer = new XmlWriter(appendable)) {
976                 writer.startElement("ReportConfiguration")
977                         .attribute("addingLicenses", Boolean.toString(addingLicenses))
978                         .attribute("addingLicensesForced", Boolean.toString(addingLicensesForced))
979                         .attribute("listFamilies", listFamilies.name())
980                         .attribute("listLicenses", listLicenses.name())
981                         .attribute("dryRun", Boolean.toString(dryRun))
982                         .attribute("archiveProcessing", getArchiveProcessing().name())
983                         .attribute("standardProcessing", getStandardProcessing().name())
984                         .attribute("stylesheet", styleSheet.name())
985                         .attribute("output", out.name());
986                 if (StringUtils.isNotEmpty(copyrightMessage)) {
987                     writer.startElement("copyrightMessage").content(copyrightMessage).closeElement();
988                 }
989                 writer.startElement("sources");
990                 for (File f : sources) {
991                     writer.startElement("source").attribute("name", f.toString()).closeElement();
992                 }
993                 writer.closeElement("sources").startElement("reportables");
994                 for (Reportable reportable : reportables) {
995                     writer.startElement("reportable")
996                             .attribute("baseName", reportable.name().getBaseName())
997                             .attribute("name", reportable.name().toString())
998                             .attribute("class", reportable.getClass().getName()).closeElement();
999                 }
1000                 writer.closeElement();
1001 
1002                 exclusionProcessor.serDes().serialize(writer);
1003 
1004                 writer.startElement("claimValidator");
1005                 for (ClaimStatistic.Counter counter : ClaimStatistic.Counter.values()) {
1006                     writer.startElement("claimCounter")
1007                             .attribute("name", counter.name()).attribute("min", Integer.toString(claimValidator.getMin(counter)))
1008                             .attribute("max", Integer.toString(claimValidator.getMax(counter))).closeElement();
1009                 }
1010                 writer.closeElement();
1011             } catch (IOException e) {
1012                 throw e;
1013             } catch (Exception e) {
1014                 throw new IOException(e);
1015             }
1016         }
1017 
1018         public void deserialize(final IOSupplier<InputStream> inputStreamSupplier, final DocumentName workingDirectory) throws IOException {
1019             org.w3c.dom.Document document;
1020             try (InputStream stream = inputStreamSupplier.get()) {
1021                 document = StandardXmlFactory.documentBuilder().parse(stream);
1022             } catch (SAXException e) {
1023                 throw new IOException("Unable to read input", e);
1024             }
1025             Node node = document.getDocumentElement();
1026             if (!node.getNodeName().equals("ReportConfiguration")) {
1027                 throw new IOException("Invalid ReportConfiguration");
1028             }
1029             Map<String, String> attributes = XMLConfigurationReader.attributes(node);
1030             addingLicenses = Boolean.parseBoolean(attributes.get("addingLicenses"));
1031             addingLicensesForced = Boolean.parseBoolean(attributes.get("addingLicensesForced"));
1032             listFamilies = LicenseFilter.valueOf(attributes.get("listFamilies"));
1033             listLicenses = LicenseFilter.valueOf(attributes.get("listLicenses"));
1034             dryRun = Boolean.parseBoolean(attributes.get("dryRun"));
1035             archiveProcessing = Processing.valueOf(attributes.get("archiveProcessing"));
1036             standardProcessing = Processing.valueOf(attributes.get("standardProcessing"));
1037             String styleName = attributes.get("stylesheet");
1038             if (styleName != null) {
1039                 styleSheet = StyleSheets.getStyleSheet(styleName);
1040             }
1041             String outputName = attributes.get("output");
1042             if (outputName != null) {
1043                 if (outputName.equals(ReportConfiguration.SYSTEM_OUT.name())) {
1044                     out = ReportConfiguration.SYSTEM_OUT;
1045                 } else {
1046                     out = IODescriptor.output(outputName, workingDirectory);
1047                 }
1048             }
1049 
1050             XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("copyrightMessage"),
1051                     lNode -> setCopyrightMessage(lNode.getTextContent()));
1052 
1053             XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("source"), lNode -> {
1054                 Map<String, String> nAttributes = XMLConfigurationReader.attributes(lNode);
1055                 addSource(new File(nAttributes.get("name")));
1056             });
1057 
1058             // Deserialize the reportables.
1059             XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("reportable"), lNode -> {
1060                 Map<String, String> nAttributes = XMLConfigurationReader.attributes(lNode);
1061                 DocumentName documentName = DocumentName.builder().setBaseName(nAttributes.get("baseName"))
1062                         .setName(nAttributes.get("name")).build();
1063                 addSource(new DeserializedReportable(documentName));
1064             });
1065 
1066             exclusionProcessor.serDes().deserialize(document.getElementsByTagName("ExclusionProcessor").item(0));
1067 
1068             XMLConfigurationReader.nodeListConsumer(document.getElementsByTagName("claimCounter"), lNode -> {
1069                 Map<String, String> nAttributes = XMLConfigurationReader.attributes(lNode);
1070                 ClaimStatistic.Counter counter = ClaimStatistic.Counter.valueOf(nAttributes.get("name"));
1071                 claimValidator.setMin(counter, Integer.parseInt(nAttributes.get("min")));
1072                 claimValidator.setMax(counter, Integer.parseInt(nAttributes.get("max")));
1073             });
1074         }
1075     }
1076 
1077     /**
1078      * A record that identifies a deserialized reportable. Deserialized reportables are not executable.
1079      * @param name the name of the reportable.
1080      */
1081     private record DeserializedReportable(DocumentName name) implements Reportable {
1082         @Override
1083         public void run(final RatReport report) throws RatException {
1084             throw new RatException("Attempt to run a deserialized reportable");
1085         }
1086     }
1087 }