diff --git a/apache-rat-core/src/main/java/org/apache/rat/DeprecationReporter.java b/apache-rat-core/src/main/java/org/apache/rat/DeprecationReporter.java
index fe3acda18..c707c6065 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/DeprecationReporter.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/DeprecationReporter.java
@@ -42,9 +42,14 @@ private DeprecationReporter() {
}
/**
- * The consumer that is used for deprecation reporting.
+ * The per-thread consumer that is used for deprecation reporting.
+ *
+ * Stored in a {@link ThreadLocal} so that each thread (e.g. parallel Maven
+ * reactor threads) gets its own reporter and does not interfere with
+ * reporters set by other threads.
+ *
*/
- private static Consumer consumer = getDefault();
+ private static final ThreadLocal> CONSUMER = ThreadLocal.withInitial(DeprecationReporter::getDefault);
/**
* Get the default reporter.
@@ -70,7 +75,7 @@ public static Consumer getDefault() {
* @return The consumer that will log usage of deprecated operations to the default log.
*/
public static Consumer getLogReporter() {
- return consumer;
+ return CONSUMER.get();
}
/**
@@ -78,14 +83,14 @@ public static Consumer getLogReporter() {
* @param consumer The consumer that will do the reporting.
*/
public static void setLogReporter(final Consumer consumer) {
- DeprecationReporter.consumer = consumer;
+ CONSUMER.set(consumer);
}
/**
- * Rests the consumer to the default consumer.
+ * Resets the consumer to the default consumer.
*/
public static void resetLogReporter() {
- DeprecationReporter.consumer = getDefault();
+ CONSUMER.set(getDefault());
}
/**
diff --git a/apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java b/apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java
index 2d4bdd636..ccf5b40fe 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/OptionCollection.java
@@ -120,6 +120,16 @@ public static ReportConfiguration parseCommands(final File workingDirectory, fin
/**
* Parses the standard options to create a ReportConfiguration.
+ *
+ * This method is {@code synchronized} because it uses shared mutable state:
+ * the {@link Arg} enum's {@code OptionGroup} instances (whose {@code selected}
+ * field is mutated by {@link DefaultParser#parse}), and
+ * {@link org.apache.rat.commandline.Converters#FILE_CONVERTER} (whose
+ * {@code workingDirectory} field is set during argument processing).
+ * Without synchronization, parallel Maven reactor threads (e.g. {@code mvn -T4})
+ * corrupt each other's parse state, causing options like {@code --input-exclude}
+ * to be silently skipped.
+ *
*
* @param workingDirectory The directory to resolve relative file names against.
* @param args the arguments to parse.
@@ -128,7 +138,7 @@ public static ReportConfiguration parseCommands(final File workingDirectory, fin
* @return a ReportConfiguration or {@code null} if Help was printed.
* @throws IOException on error.
*/
- public static ReportConfiguration parseCommands(final File workingDirectory, final String[] args,
+ public static synchronized ReportConfiguration parseCommands(final File workingDirectory, final String[] args,
final Consumer helpCmd, final boolean noArgs) throws IOException {
Options opts = buildOptions();
diff --git a/apache-rat-core/src/main/java/org/apache/rat/analysis/matchers/SPDXMatcherFactory.java b/apache-rat-core/src/main/java/org/apache/rat/analysis/matchers/SPDXMatcherFactory.java
index 20869bc2f..2568d7dd3 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/analysis/matchers/SPDXMatcherFactory.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/analysis/matchers/SPDXMatcherFactory.java
@@ -40,19 +40,29 @@
* SPDX identifiers are specified by the Software Package Data Exchange(R) also
* known as SPDX(R) project from the Linux foundation.
*
+ *
+ * Each factory instance maintains its own matcher map and per-document match
+ * state ({@code lastMatch}, {@code checked}). In multi-threaded environments
+ * (e.g. parallel Maven builds), use {@link #newInstance()} or a
+ * {@code ThreadLocal} to obtain a per-thread factory
+ * instead of the shared {@link #INSTANCE}.
+ *
*
* @see List of Ids at spdx.dev
*/
public final class SPDXMatcherFactory {
/**
- * The collection of all matchers produced by this factory.
+ * The collection of all matchers produced by this factory instance.
*/
- private static final Map MATCHER_MAP = new HashMap<>();
+ private final Map matcherMap = new HashMap<>();
/**
- * The instance of this factory.
+ * The shared instance of this factory.
+ * @deprecated Not thread-safe. Use {@link #newInstance()} to create
+ * per-thread instances instead. Will be removed in 1.0.0.
*/
+ @Deprecated
public static final SPDXMatcherFactory INSTANCE = new SPDXMatcherFactory();
/**
@@ -77,12 +87,25 @@ public final class SPDXMatcherFactory {
private boolean checked;
/**
- * Constructor.
+ * Constructor. Creates a new factory with its own matcher map and match state.
*/
private SPDXMatcherFactory() {
lastMatch = new HashSet<>();
}
+ /**
+ * Creates a new SPDXMatcherFactory instance.
+ *
+ * Use this method to obtain a per-thread factory for multi-threaded
+ * environments instead of the shared {@link #INSTANCE}.
+ *
+ *
+ * @return a new SPDXMatcherFactory instance.
+ */
+ public static SPDXMatcherFactory newInstance() {
+ return new SPDXMatcherFactory();
+ }
+
/**
* Reset the matching for the next document.
*/
@@ -101,12 +124,7 @@ public Match create(final String spdxId) {
if (StringUtils.isBlank(spdxId)) {
throw new ConfigurationException("'SPDX' type matcher requires a name");
}
- Match matcher = MATCHER_MAP.get(spdxId);
- if (matcher == null) {
- matcher = new Match(spdxId);
- MATCHER_MAP.put(spdxId, matcher);
- }
- return matcher;
+ return matcherMap.computeIfAbsent(spdxId, Match::new);
}
/**
diff --git a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/StandardCollection.java b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/StandardCollection.java
index 7ec79ae66..ca0b69004 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/StandardCollection.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/config/exclusion/StandardCollection.java
@@ -27,6 +27,7 @@
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
+import java.util.function.Supplier;
import org.apache.rat.config.exclusion.fileProcessors.AbstractFileProcessorBuilder;
import org.apache.rat.config.exclusion.fileProcessors.BazaarIgnoreBuilder;
@@ -56,7 +57,7 @@ public enum StandardCollection {
* The files and directories created by a Bazaar source code control based tool.
*/
BAZAAR("The files and directories created by a Bazaar source code control based tool.",
- Arrays.asList("**/.bzr/**", "**/.bzrignore"), null, new BazaarIgnoreBuilder()),
+ Arrays.asList("**/.bzr/**", "**/.bzrignore"), null, BazaarIgnoreBuilder::new),
/**
* The files and directories created by a Bitkeeper source code control based tool.
*/
@@ -75,7 +76,7 @@ public enum StandardCollection {
"**/*.orig", "**/*.rej", "**/.del-*",
"**/*.a", "**/*.old", "**/*.o", "**/*.obj", "**/*.so", "**/*.exe",
"**/*.Z", "**/*.elc", "**/*.ln", "**/core"),
- null, new CVSIgnoreBuilder()),
+ null, CVSIgnoreBuilder::new),
/**
* The files and directories created by a DARCS source code control based tool.
*/
@@ -96,7 +97,7 @@ null, new CVSIgnoreBuilder()),
"and (unless RAT_NO_GIT_GLOBAL_IGNORE is specified) the global gitignore.",
Arrays.asList("**/.git/**", "**/.gitignore"),
null,
- new GitIgnoreBuilder()
+ GitIgnoreBuilder::new
),
/**
* The hidden directories. Directories with names that start with {@code .}
@@ -170,7 +171,7 @@ public String toString() {
* The files and directories created by a Mercurial source code control based tool.
*/
MERCURIAL("The files and directories created by a Mercurial source code control based tool.",
- Arrays.asList("**/.hg/**", "**/.hgignore"), null, new HgIgnoreBuilder()),
+ Arrays.asList("**/.hg/**", "**/.hgignore"), null, HgIgnoreBuilder::new),
/**
* The set of miscellaneous files generally left by editors and the like.
*/
@@ -227,17 +228,24 @@ public String toString() {
private final Collection patterns;
/** A document name matcher supplier to create a document name matcher. May be null */
private final DocumentNameMatcher staticDocumentNameMatcher;
- /** The AbstractFileProcessorBuilder to process the exclude file associated with this exclusion. May be {@code null}. */
- private final AbstractFileProcessorBuilder fileProcessorBuilder;
+ /**
+ * Supplier for the AbstractFileProcessorBuilder. A Supplier is used instead of a direct
+ * instance because these builders contain mutable state ({@code levelBuilders}, and in the
+ * case of {@link HgIgnoreBuilder}, a mutable {@code state} field). Enum constants are
+ * singletons, so storing a shared builder instance would cause concurrent threads in a
+ * parallel Maven build to corrupt each other's state. The supplier creates a fresh builder
+ * for each invocation, ensuring thread safety.
+ */
+ private final Supplier fileProcessorBuilderSupplier;
/** The description of this collection */
private final String desc;
StandardCollection(final String desc, final Collection patterns, final DocumentNameMatcher documentNameMatcher,
- final AbstractFileProcessorBuilder fileProcessorBuilder) {
+ final Supplier fileProcessorBuilderSupplier) {
this.desc = desc;
this.patterns = patterns == null ? Collections.emptyList() : new HashSet<>(patterns);
this.staticDocumentNameMatcher = documentNameMatcher;
- this.fileProcessorBuilder = fileProcessorBuilder;
+ this.fileProcessorBuilderSupplier = fileProcessorBuilderSupplier;
}
/**
@@ -294,8 +302,8 @@ public Set patterns() {
public ExtendedIterator fileProcessorBuilder() {
List lst = new ArrayList<>();
for (StandardCollection sc : getCollections()) {
- if (sc.fileProcessorBuilder != null) {
- lst.add(sc.fileProcessorBuilder);
+ if (sc.fileProcessorBuilderSupplier != null) {
+ lst.add(sc.fileProcessorBuilderSupplier.get());
}
}
return ExtendedIterator.create(lst.iterator());
diff --git a/apache-rat-core/src/main/java/org/apache/rat/configuration/MatcherBuilderTracker.java b/apache-rat-core/src/main/java/org/apache/rat/configuration/MatcherBuilderTracker.java
index aa7210efb..d19fbfec2 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/configuration/MatcherBuilderTracker.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/configuration/MatcherBuilderTracker.java
@@ -21,9 +21,9 @@
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.Collections;
-import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.WordUtils;
@@ -88,7 +88,7 @@ public static AbstractBuilder getMatcherBuilder(final String name) {
}
private MatcherBuilderTracker() {
- matcherBuilders = new HashMap<>();
+ matcherBuilders = new ConcurrentHashMap<>();
}
/**
diff --git a/apache-rat-core/src/main/java/org/apache/rat/configuration/builders/SpdxBuilder.java b/apache-rat-core/src/main/java/org/apache/rat/configuration/builders/SpdxBuilder.java
index 2a72fd2b7..ad49ec137 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/configuration/builders/SpdxBuilder.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/configuration/builders/SpdxBuilder.java
@@ -27,9 +27,23 @@
/**
* A builder for SPDX matchers.
+ *
+ * Uses a {@code ThreadLocal} factory so that each thread in a parallel Maven
+ * build gets its own {@link SPDXMatcherFactory} instance with isolated match
+ * state. This prevents two threads scanning different documents from
+ * cross-contaminating each other's SPDX identifier results.
+ *
*/
@MatcherBuilder(SPDXMatcherFactory.Match.class)
public class SpdxBuilder extends AbstractBuilder {
+
+ /**
+ * Per-thread SPDXMatcherFactory. Each thread gets its own factory with
+ * its own matcher map and per-document match state ({@code lastMatch},
+ * {@code checked}).
+ */
+ private static final ThreadLocal FACTORY = ThreadLocal.withInitial(SPDXMatcherFactory::newInstance);
+
/** The SPDX name */
private String name;
@@ -64,7 +78,7 @@ public AbstractBuilder setId(final String id) {
@Override
public SPDXMatcherFactory.Match build() {
- return SPDXMatcherFactory.INSTANCE.create(name);
+ return FACTORY.get().create(name);
}
@Override
diff --git a/apache-rat-core/src/main/java/org/apache/rat/utils/DefaultLog.java b/apache-rat-core/src/main/java/org/apache/rat/utils/DefaultLog.java
index 0f65d6db5..4fd6b7833 100644
--- a/apache-rat-core/src/main/java/org/apache/rat/utils/DefaultLog.java
+++ b/apache-rat-core/src/main/java/org/apache/rat/utils/DefaultLog.java
@@ -22,30 +22,35 @@
/**
* A default implementation of Log that writes to {@code System.out} and {@code System.err}.
+ *
+ * The singleton instance is stored in a {@link ThreadLocal} so that each thread
+ * (e.g. parallel Maven reactor threads) gets its own logger and does not
+ * interfere with loggers set by other threads.
+ *
*/
public final class DefaultLog implements Log {
/**
- * The instance of the default log.
+ * The per-thread instance of the default log.
*/
- private static Log instance = new DefaultLog();
+ private static final ThreadLocal INSTANCE = ThreadLocal.withInitial(DefaultLog::new);
/**
- * Retrieves the DefaultLog instance.
+ * Retrieves the DefaultLog instance for the current thread.
* @return the Default log instance.
*/
public static Log getInstance() {
- return instance;
+ return INSTANCE.get();
}
/**
- * Sets the default log instance.
- * If not set an instance of DefaultLog will be returned
+ * Sets the default log instance for the current thread.
+ * If not set an instance of DefaultLog will be returned.
* @param newInstance a Log to use as the default.
* @return the old instance.
*/
public static Log setInstance(final Log newInstance) {
- Log result = instance;
- instance = newInstance == null ? new DefaultLog() : newInstance;
+ Log result = INSTANCE.get();
+ INSTANCE.set(newInstance == null ? new DefaultLog() : newInstance);
return result;
}
diff --git a/apache-rat-plugin/src/it/RAT-573/invoker.properties b/apache-rat-plugin/src/it/RAT-573/invoker.properties
new file mode 100644
index 000000000..9e9609c82
--- /dev/null
+++ b/apache-rat-plugin/src/it/RAT-573/invoker.properties
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+invoker.goals = -T4 clean apache-rat:check
diff --git a/apache-rat-plugin/src/it/RAT-573/module1/pom.xml b/apache-rat-plugin/src/it/RAT-573/module1/pom.xml
new file mode 100644
index 000000000..030564c0a
--- /dev/null
+++ b/apache-rat-plugin/src/it/RAT-573/module1/pom.xml
@@ -0,0 +1,26 @@
+
+
+
+ 4.0.0
+
+ org.apache.rat.test
+ rat573
+ 1.0
+
+ module1
+
diff --git a/apache-rat-plugin/src/it/RAT-573/module1/src.apt b/apache-rat-plugin/src/it/RAT-573/module1/src.apt
new file mode 100644
index 000000000..3e7e7d9e0
--- /dev/null
+++ b/apache-rat-plugin/src/it/RAT-573/module1/src.apt
@@ -0,0 +1,11 @@
+~~ Yet Another License, just for test purposes
+
+ --------------
+ Some text file
+ --------------
+
+Some text file
+
+ This is a text file, which intentionally has no Apache License Header.
+ Instead, it contains a dummy license header. The Rat plugin should
+ accept it with a proper custom license matcher.
\ No newline at end of file
diff --git a/apache-rat-plugin/src/it/RAT-573/module2/pom.xml b/apache-rat-plugin/src/it/RAT-573/module2/pom.xml
new file mode 100644
index 000000000..704ca6988
--- /dev/null
+++ b/apache-rat-plugin/src/it/RAT-573/module2/pom.xml
@@ -0,0 +1,26 @@
+
+
+
+ 4.0.0
+
+ org.apache.rat.test
+ rat573
+ 1.0
+
+ module2
+
diff --git a/apache-rat-plugin/src/it/RAT-573/module2/src.apt b/apache-rat-plugin/src/it/RAT-573/module2/src.apt
new file mode 100644
index 000000000..3e7e7d9e0
--- /dev/null
+++ b/apache-rat-plugin/src/it/RAT-573/module2/src.apt
@@ -0,0 +1,11 @@
+~~ Yet Another License, just for test purposes
+
+ --------------
+ Some text file
+ --------------
+
+Some text file
+
+ This is a text file, which intentionally has no Apache License Header.
+ Instead, it contains a dummy license header. The Rat plugin should
+ accept it with a proper custom license matcher.
\ No newline at end of file
diff --git a/apache-rat-plugin/src/it/RAT-573/module3/pom.xml b/apache-rat-plugin/src/it/RAT-573/module3/pom.xml
new file mode 100644
index 000000000..4af5df36e
--- /dev/null
+++ b/apache-rat-plugin/src/it/RAT-573/module3/pom.xml
@@ -0,0 +1,26 @@
+
+
+
+ 4.0.0
+
+ org.apache.rat.test
+ rat573
+ 1.0
+
+ module3
+
diff --git a/apache-rat-plugin/src/it/RAT-573/module3/src.apt b/apache-rat-plugin/src/it/RAT-573/module3/src.apt
new file mode 100644
index 000000000..3e7e7d9e0
--- /dev/null
+++ b/apache-rat-plugin/src/it/RAT-573/module3/src.apt
@@ -0,0 +1,11 @@
+~~ Yet Another License, just for test purposes
+
+ --------------
+ Some text file
+ --------------
+
+Some text file
+
+ This is a text file, which intentionally has no Apache License Header.
+ Instead, it contains a dummy license header. The Rat plugin should
+ accept it with a proper custom license matcher.
\ No newline at end of file
diff --git a/apache-rat-plugin/src/it/RAT-573/pom.xml b/apache-rat-plugin/src/it/RAT-573/pom.xml
new file mode 100644
index 000000000..07ac10712
--- /dev/null
+++ b/apache-rat-plugin/src/it/RAT-573/pom.xml
@@ -0,0 +1,46 @@
+
+
+
+ 4.0.0
+ org.apache.rat.test
+ rat573
+ 1.0
+ pom
+
+ module1
+ module2
+ module3
+
+
+
+
+ org.apache.rat
+ apache-rat-plugin
+ @pom.version@
+
+
+ STANDARDS:0
+
+
+ **/src.apt
+
+
+
+
+
+
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index e536bd97f..fff47be6a 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -68,6 +68,12 @@ in order to be properly linked in site reports.
-->
+
+ Fix NPE with parallel builds in SCM ignore parsers.
+
+
+ Make RAT safe for parallel builds.
+
Internal change: Introduced a Reporter.Output class that encapsulates the execution configuration, the generated XML document, and the ClaimStatistic containing counts for file types, licenses, license categories, and other all of RAT run statistics.