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.Arrays;
22  import java.util.regex.Matcher;
23  import java.util.regex.Pattern;
24  import java.util.stream.Collectors;
25  
26  import org.apache.commons.lang3.StringUtils;
27  import org.apache.rat.ConfigurationException;
28  import org.apache.rat.analysis.IHeaders;
29  import org.apache.rat.config.parameters.ComponentType;
30  import org.apache.rat.config.parameters.ConfigComponent;
31  
32  /**
33   * Matches a typical Copyright header line only based on a regex pattern which
34   * allows for one (starting) year or year range, and a configurable copyright
35   * owner.<br>
36   * <br>
37   * The matching is done case-insensitive<br>
38   * <br>
39   * Example supported Copyright header lines, using copyright owner
40   * &quot;FooBar&quot;
41   * <ul>
42   * <li>Copyright 2010 FooBar.</li>
43   * <li>Copyright 2010-2012 FooBar.</li>
44   * <li>copyright 2012 foobar</li>
45   * </ul>
46   * <p>
47   * Note also that the copyright owner is appended to the regex pattern and so
48   * can support additional regex but also requires escaping where needed,<br>
49   * e.g. use &quot;FooBar \\(www\\.foobar\\.com\\)&quot; or &quot;FooBar
50   * \\Q(www.foobar.com)\\E&quot; to match &quot;FooBar (www.foobar.com)&quot;
51   * </p>
52   * <p>
53   * The matcher also accepts "(C)", "(c)", and "©" in place of (or in addition
54   * to) the "Copyright" or "copyright" keyword
55   * </p>
56   */
57  @ConfigComponent(type = ComponentType.MATCHER, name = "copyright",
58          desc = "A matcher that matches Copyright text. " +
59                  "Uses regular expressions and so should only be used when looking for copyrights with specific " +
60                  "patterns that are not caught by a standard text matcher. This matcher will match \"(C)\", \"copyright\", " +
61                  "or \"©\". (text is not case sensitive). It will also match things like Copyright (c) joe 1995 as well " +
62                  "as Copyright (C) 1995 joe and Copyright (C) joe 1995.")
63  public final class CopyrightMatcher extends AbstractHeaderMatcher {
64      /** String to build a pattern to match the various recognized copyright symbols */
65      private static final String COPYRIGHT_SYMBOL_DEFN = "\\([Cc]\\)|©|\\&[Cc][Oo][Pp][Yy]\\;";
66      /** String to build a pattern to match symbols or word "copyright" */
67      private static final String COPYRIGHT_PATTERN_DEFN = "(\\b)?" + COPYRIGHT_SYMBOL_DEFN + "|Copyright\\b";
68      /** The compiled pattern from {@link #COPYRIGHT_PATTERN_DEFN} */
69      private static final Pattern COPYRIGHT_PATTERN = Pattern.compile(COPYRIGHT_PATTERN_DEFN);
70      /** The string to build a Pattern to match a copyright with a single part (date or name) */
71      private static final String ONE_PART = "\\W+((" + COPYRIGHT_SYMBOL_DEFN + ")\\W+)?%s";
72      /** The string to build a Pattern to match a copyright with both name and date */
73      private static final String TWO_PART = "\\W+((" + COPYRIGHT_SYMBOL_DEFN + ")\\W+)?%s,?\\W+%s";
74      /** Format string to build a pattern to match two dates */
75      private static final String DOUBLE_DATE_FMT = "%s\\W*-\\W*%s";
76      /**
77       * The maximum number of characters following the "copyright" keyword that are
78       * inspected when looking for a date/owner. The date and owner immediately
79       * follow the keyword, and bounding the search prevents the regex engine from
80       * backtracking over an arbitrarily large header (which can hang RAT on files
81       * like large YAML documents).
82       */
83      private static final int MAX_COPYRIGHT_MATCH_LENGTH = 1000;
84      /** String to build a pattern to match an arbitrary date (year) */
85      private static final String ARBITRARY_DATE = "[0-9]{4}";
86      /** The built Pattern for matching "Copyright date owner" */
87      private final Pattern dateOwnerPattern;
88      /** The built pattern for matching "Copyright owner date" */
89      private final Pattern ownerDatePattern;
90      /** The start date of the copyright. May be null. */
91      @ConfigComponent(type = ComponentType.PARAMETER, desc = "The initial year of the copyright if any")
92      private final String start;
93      /** The end date of the copyright. May be null. */
94      @ConfigComponent(type = ComponentType.PARAMETER, desc = "The last year the copyright. Only valid with 'start'")
95      private final String end;
96      /** The owner of the copyright. May be null */
97      @ConfigComponent(type = ComponentType.PARAMETER, desc = "The owner of the copyright")
98      private final String owner;
99  
100     /**
101      * Constructs the CopyrightMatcher with the specified start, stop and owner
102      * strings and a unique random id.
103      *
104      * @param start the start date for the copyright, may be null.
105      * @param end the stop date for the copyright, may be null. May not be
106      * specified if start is not specified.
107      * @param owner the owner of the copyright, may be null.
108      */
109     public CopyrightMatcher(final String start, final String end, final String owner) {
110         this(null, start, end, owner);
111     }
112 
113     private static void assertNumber(final String label, final String value) {
114         try {
115             if (StringUtils.isNotEmpty(value)) {
116                 Integer.parseInt(value);
117             }
118         } catch (NumberFormatException e) {
119             throw new ConfigurationException(String.format("'%s' must be numeric (value provided: '%s')", label, value));
120         }
121     }
122 
123     /**
124      * Converts the owner string into a set of tokens that may be separated by multiple whitespaces and comment characters.
125      * Any non word character may appear between the tokens. Tokens are escaped so that any text is not interpreted as
126      * regex characters.
127      * @param owner the owner string.
128      * @return the regex string to match the owner string.
129      */
130     private String parseOwner(final String owner) {
131         return Arrays.stream(owner.split("\\s+")).map(s -> "\\Q" + s + "\\E").collect(Collectors.joining("\\W+"));
132     }
133 
134     /**
135      * Constructs the CopyrightMatcher with the specified start, stop and owner
136      * strings.
137      *
138      * @param id the id for the matcher.
139      * @param start the start date for the copyright, may be null.
140      * @param end the end date for the copyright, may be null. May not be
141      * specified if start is not specified.
142      * @param owner the owner of the copyright. May be null.
143      */
144     public CopyrightMatcher(final String id, final String start, final String end, final String owner) {
145         super(id);
146         if (StringUtils.isBlank(start) && !StringUtils.isBlank(end)) {
147             throw new ConfigurationException("'end' may not be set if 'start' is not set.");
148         }
149         assertNumber("start", start);
150         assertNumber("end", end);
151         this.start = start;
152         this.end = end;
153         this.owner = owner;
154         String dateDefn = "";
155         if (StringUtils.isNotEmpty(start)) {
156             if (StringUtils.isNotEmpty(end)) {
157                 dateDefn = String.format(DOUBLE_DATE_FMT, this.start, this.end);
158             } else {
159                 dateDefn = this.start;
160             }
161         }
162         if (StringUtils.isEmpty(owner)) {
163             // no owner
164             if (StringUtils.isEmpty(dateDefn)) {
165                 dateDefn = ARBITRARY_DATE;
166             }
167             dateOwnerPattern = Pattern.compile(String.format(ONE_PART, dateDefn));
168             ownerDatePattern = null;
169         } else {
170             String formattedOwner = parseOwner(owner);
171             if (StringUtils.isEmpty(dateDefn)) {
172                 dateDefn = String.format(DOUBLE_DATE_FMT, "(((" + ARBITRARY_DATE, ")?" + ARBITRARY_DATE + "))?");
173                 dateOwnerPattern = Pattern.compile(String.format(TWO_PART, dateDefn, formattedOwner));
174                 ownerDatePattern = Pattern.compile(String.format(ONE_PART, formattedOwner));
175             } else {
176                 dateOwnerPattern = Pattern.compile(String.format(TWO_PART, dateDefn, formattedOwner));
177                 ownerDatePattern = Pattern.compile(String.format(TWO_PART, formattedOwner, dateDefn));
178             }
179         }
180     }
181 
182     /**
183      * Gets the start date of the copyright.
184      * @return the start date of the copyright or {@code null} if not set
185      */
186     public String getStart() {
187         return start;
188     }
189 
190     /**
191      * Gets the end date of the copyright.
192      * @return the end date of the copyright or {@code null} if not set
193      */
194     public String getEnd() {
195         return end;
196     }
197 
198     /**
199      * Gets the owner of the copyright.
200      * @return the owner of the copyright or {@code null} if not set
201      */
202     public String getOwner() {
203         return owner;
204     }
205 
206     @Override
207     public boolean matches(final IHeaders headers) {
208         String lowerLine = headers.raw().toLowerCase();
209         if (lowerLine.contains("copyright") || lowerLine.contains("(c)") || lowerLine.contains("©") ||
210                 lowerLine.contains("&copy;")) {
211             Matcher matcher = COPYRIGHT_PATTERN.matcher(headers.raw());
212             if (matcher.find()) {
213                 String buffer = headers.raw().substring(matcher.end());
214                 // The date/owner must immediately follow the "copyright" keyword. Use
215                 // lookingAt() (anchored to the start of the buffer) rather than find()
216                 // so the regex engine does not re-scan the entire (potentially large)
217                 // raw header looking for a match, which can cause catastrophic
218                 // backtracking on files with many otherwise non-matching lines.
219                 // Additionally bound the inspected region so any single match attempt
220                 // cannot scan an unbounded amount of header data.
221                 if (buffer.length() > MAX_COPYRIGHT_MATCH_LENGTH) {
222                     buffer = buffer.substring(0, MAX_COPYRIGHT_MATCH_LENGTH);
223                 }
224                 matcher = dateOwnerPattern.matcher(buffer);
225                 if (matcher.lookingAt()) {
226                     return true;
227                 }
228                 if (ownerDatePattern != null) {
229                     matcher = ownerDatePattern.matcher(buffer);
230                     return matcher.lookingAt();
231                 }
232             }
233         }
234         return false;
235     }
236 }