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.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 }