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