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.analysis.matchers;
20
21 import java.util.HashMap;
22 import java.util.HashSet;
23 import java.util.Map;
24 import java.util.Objects;
25 import java.util.Set;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
28
29 import org.apache.commons.lang3.StringUtils;
30 import org.apache.rat.ConfigurationException;
31 import org.apache.rat.analysis.IHeaders;
32 import org.apache.rat.config.parameters.ComponentType;
33 import org.apache.rat.config.parameters.ConfigComponent;
34
35 /**
36 * Defines a factory to produce matchers for an SPDX tag. SPDX tag is of the
37 * format {@code SPDX-License-Identifier: short-name} where {@code short-name}
38 * matches the regex pattern [A-Za-z0-9\.\-]+
39 * <p>
40 * SPDX identifiers are specified by the Software Package Data Exchange(R) also
41 * known as SPDX(R) project from the Linux foundation.
42 * </p>
43 * <p>
44 * Each factory instance maintains its own matcher map and per-document match
45 * state ({@code lastMatch}, {@code checked}). In multi-threaded environments
46 * (e.g. parallel Maven builds), use {@link #newInstance()} or a
47 * {@code ThreadLocal<SPDXMatcherFactory>} to obtain a per-thread factory
48 * instead of the shared {@link #INSTANCE}.
49 * </p>
50 *
51 * @see <a href="https://spdx.dev/ids/">List of Ids at spdx.dev</a>
52 */
53 public final class SPDXMatcherFactory {
54
55 /**
56 * The collection of all matchers produced by this factory instance.
57 */
58 private final Map<String, SPDXMatcherFactory.Match> matcherMap = new HashMap<>();
59
60 /**
61 * The shared instance of this factory.
62 * @deprecated Not thread-safe. Use {@link #newInstance()} to create
63 * per-thread instances instead. Will be removed in 1.0.0.
64 */
65 @Deprecated
66 public static final SPDXMatcherFactory INSTANCE = new SPDXMatcherFactory();
67
68 /**
69 * The text for the group selector.
70 */
71 static final String LICENSE_IDENTIFIER = "SPDX-License-Identifier:";
72
73 /**
74 * The regular expression to locate the SPDX license identifier in the text
75 * stream.
76 */
77 private static final Pattern GROUP_SELECTOR = Pattern.compile(".*" + LICENSE_IDENTIFIER + "\\s([A-Za-z0-9\\.\\-]+)");
78
79 /**
80 * The set of SPDX Ids that matched the last text.
81 */
82 private final Set<String> lastMatch;
83
84 /**
85 * Flag to indicate this document has been checked for SPDX tags.
86 */
87 private boolean checked;
88
89 /**
90 * Constructor. Creates a new factory with its own matcher map and match state.
91 */
92 private SPDXMatcherFactory() {
93 lastMatch = new HashSet<>();
94 }
95
96 /**
97 * Creates a new SPDXMatcherFactory instance.
98 * <p>
99 * Use this method to obtain a per-thread factory for multi-threaded
100 * environments instead of the shared {@link #INSTANCE}.
101 * </p>
102 *
103 * @return a new SPDXMatcherFactory instance.
104 */
105 public static SPDXMatcherFactory newInstance() {
106 return new SPDXMatcherFactory();
107 }
108
109 /**
110 * Reset the matching for the next document.
111 */
112 private void reset() {
113 lastMatch.clear();
114 checked = false;
115 }
116
117 /**
118 * Creates the SPDX matcher.
119 *
120 * @param spdxId the SPDX name to match.
121 * @return a SPDX matcher.
122 */
123 public Match create(final String spdxId) {
124 if (StringUtils.isBlank(spdxId)) {
125 throw new ConfigurationException("'SPDX' type matcher requires a name");
126 }
127 return matcherMap.computeIfAbsent(spdxId, Match::new);
128 }
129
130 /**
131 * Each matcher calls this method to present the documentText it is working on.
132 *
133 * @param documentText The documentText the caller is looking at.
134 * @param caller the Match that is calling this method.
135 * @return true if the caller matches the text.
136 */
137 private boolean check(final String documentText, final Match caller) {
138 /*
139 If the documentText has not been seen yet see if we can extract the SPDX id from the documentText.
140 If so then see for each match extract and add the name to lastMatch.
141 */
142 if (!checked) {
143 checked = true;
144 if (documentText.contains(LICENSE_IDENTIFIER)) {
145 Matcher matcher = GROUP_SELECTOR.matcher(documentText);
146 while (matcher.find()) {
147 lastMatch.add(matcher.group(1));
148 }
149 }
150 }
151 // see if the caller is in the lastMatch.
152 return lastMatch.contains(caller.spdxId);
153 }
154
155 /**
156 * Matches an SPDX identifier.
157 */
158 @ConfigComponent(type = ComponentType.MATCHER, name = "spdx",
159 desc = "A matcher that matches SPDX tags. SPDX tags have the form: \"SPDX-License-Identifier: short-name\", " +
160 "where short-name matches the regex pattern \"[A-Za-z0-9\\.-]+\". " +
161 "The SPDX matcher takes the short name as an argument.")
162 public class Match extends AbstractHeaderMatcher {
163 /**
164 * The SPDX identifier.
165 */
166 @ConfigComponent(type = ComponentType.PARAMETER, name = "name", desc = "The SPDX identifier string")
167 private final String spdxId;
168
169 /**
170 * Gets the name of this matcher. Same as the SPDX identifier.
171 * @return name of this matcher, that equals the SPDX identifier.
172 */
173 public String getName() {
174 return spdxId;
175 }
176
177 /**
178 * Constructor.
179 *
180 * @param spdxId A regular expression that matches the @{short-name} of the SPDX
181 * Identifier.
182 */
183 Match(final String spdxId) {
184 super("SPDX:" + spdxId);
185 Objects.requireNonNull(spdxId, "SpdxId is required");
186 this.spdxId = spdxId;
187 }
188
189 @Override
190 public boolean matches(final IHeaders headers) {
191 return SPDXMatcherFactory.this.check(headers.raw(), this);
192 }
193
194 @Override
195 public void reset() {
196 super.reset();
197 SPDXMatcherFactory.this.reset();
198 }
199 }
200 }