Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,14 @@ private DeprecationReporter() {
}

/**
* The consumer that is used for deprecation reporting.
* The per-thread consumer that is used for deprecation reporting.
* <p>
* 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.
* </p>
*/
private static Consumer<Option> consumer = getDefault();
private static final ThreadLocal<Consumer<Option>> CONSUMER = ThreadLocal.withInitial(DeprecationReporter::getDefault);

/**
* Get the default reporter.
Expand All @@ -70,22 +75,22 @@ public static Consumer<Option> getDefault() {
* @return The consumer that will log usage of deprecated operations to the default log.
*/
public static Consumer<Option> getLogReporter() {
return consumer;
return CONSUMER.get();
}

/**
* Sets the consumer that will do the reporting.
* @param consumer The consumer that will do the reporting.
*/
public static void setLogReporter(final Consumer<Option> 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());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,16 @@ public static ReportConfiguration parseCommands(final File workingDirectory, fin

/**
* Parses the standard options to create a ReportConfiguration.
* <p>
* 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.
* </p>
*
* @param workingDirectory The directory to resolve relative file names against.
* @param args the arguments to parse.
Expand All @@ -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<Options> helpCmd, final boolean noArgs) throws IOException {

Options opts = buildOptions();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* </p>
* <p>
* 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<SPDXMatcherFactory>} to obtain a per-thread factory
* instead of the shared {@link #INSTANCE}.
* </p>
*
* @see <a href="https://spdx.dev/ids/">List of Ids at spdx.dev</a>
*/
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<String, SPDXMatcherFactory.Match> MATCHER_MAP = new HashMap<>();
private final Map<String, SPDXMatcherFactory.Match> 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();
Comment thread
ottlinger marked this conversation as resolved.

/**
Expand All @@ -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.
* <p>
* Use this method to obtain a per-thread factory for multi-threaded
* environments instead of the shared {@link #INSTANCE}.
* </p>
*
* @return a new SPDXMatcherFactory instance.
*/
public static SPDXMatcherFactory newInstance() {
return new SPDXMatcherFactory();
}

/**
* Reset the matching for the next document.
*/
Expand All @@ -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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*/
Expand All @@ -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.
*/
Expand All @@ -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 .}
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -227,17 +228,24 @@ public String toString() {
private final Collection<String> 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<AbstractFileProcessorBuilder> fileProcessorBuilderSupplier;
/** The description of this collection */
private final String desc;

StandardCollection(final String desc, final Collection<String> patterns, final DocumentNameMatcher documentNameMatcher,
final AbstractFileProcessorBuilder fileProcessorBuilder) {
final Supplier<AbstractFileProcessorBuilder> fileProcessorBuilderSupplier) {
this.desc = desc;
this.patterns = patterns == null ? Collections.emptyList() : new HashSet<>(patterns);
this.staticDocumentNameMatcher = documentNameMatcher;
this.fileProcessorBuilder = fileProcessorBuilder;
this.fileProcessorBuilderSupplier = fileProcessorBuilderSupplier;
}

/**
Expand Down Expand Up @@ -294,8 +302,8 @@ public Set<String> patterns() {
public ExtendedIterator<AbstractFileProcessorBuilder> fileProcessorBuilder() {
List<AbstractFileProcessorBuilder> 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -88,7 +88,7 @@ public static AbstractBuilder getMatcherBuilder(final String name) {
}

private MatcherBuilderTracker() {
matcherBuilders = new HashMap<>();
matcherBuilders = new ConcurrentHashMap<>();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,23 @@

/**
* A builder for SPDX matchers.
* <p>
* 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.
* </p>
*/
@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<SPDXMatcherFactory> FACTORY = ThreadLocal.withInitial(SPDXMatcherFactory::newInstance);

/** The SPDX name */
private String name;

Expand Down Expand Up @@ -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
Expand Down
21 changes: 13 additions & 8 deletions apache-rat-core/src/main/java/org/apache/rat/utils/DefaultLog.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,30 +22,35 @@

/**
* A default implementation of Log that writes to {@code System.out} and {@code System.err}.
* <p>
* 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.
* </p>
*/
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<Log> 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;
}

Expand Down
16 changes: 16 additions & 0 deletions apache-rat-plugin/src/it/RAT-573/invoker.properties
Original file line number Diff line number Diff line change
@@ -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
26 changes: 26 additions & 0 deletions apache-rat-plugin/src/it/RAT-573/module1/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.rat.test</groupId>
<artifactId>rat573</artifactId>
<version>1.0</version>
</parent>
<artifactId>module1</artifactId>
</project>
11 changes: 11 additions & 0 deletions apache-rat-plugin/src/it/RAT-573/module1/src.apt
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading