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.maven.enforcer.rules.utils;
20  
21  import java.util.Collection;
22  import java.util.HashSet;
23  import java.util.Objects;
24  import java.util.function.Function;
25  import java.util.function.Predicate;
26  
27  import org.apache.maven.artifact.Artifact;
28  import org.apache.maven.artifact.versioning.ArtifactVersion;
29  import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
30  import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
31  import org.apache.maven.artifact.versioning.VersionRange;
32  import org.apache.maven.model.Dependency;
33  import org.codehaus.plexus.util.StringUtils;
34  
35  import static java.util.Optional.ofNullable;
36  
37  /**
38   * This class is used for matching Artifacts against a list of patterns.
39   *
40   * @author Jakub Senko
41   */
42  public final class ArtifactMatcher {
43  
44      /**
45       * @author I don't know
46       */
47      public static class Pattern {
48          private final String pattern;
49  
50          private final String[] parts;
51          private final Predicate<String>[] partsRegex;
52  
53          public Pattern(String pattern) {
54              if (pattern == null) {
55                  throw new NullPointerException("pattern");
56              }
57  
58              this.pattern = pattern;
59  
60              parts = pattern.split(":", 7);
61  
62              if (parts.length == 7) {
63                  throw new IllegalArgumentException("Pattern contains too many delimiters.");
64              }
65  
66              for (String part : parts) {
67                  if ("".equals(part)) {
68                      throw new IllegalArgumentException("Pattern or its part is empty.");
69                  }
70              }
71              partsRegex = new Predicate[parts.length];
72          }
73  
74          public boolean match(Artifact artifact) {
75              Objects.requireNonNull(artifact, "artifact must not be null");
76              try {
77                  return match(
78                          artifact.getGroupId(),
79                          artifact.getArtifactId(),
80                          artifact.getVersion(),
81                          artifact.getType(),
82                          artifact.getScope(),
83                          artifact.getClassifier());
84              } catch (InvalidVersionSpecificationException e) {
85                  throw new IllegalArgumentException(e);
86              }
87          }
88  
89          public boolean match(Dependency dependency) {
90              Objects.requireNonNull(dependency, "dependency must not be null");
91              try {
92                  return match(
93                          dependency.getGroupId(),
94                          dependency.getArtifactId(),
95                          dependency.getVersion(),
96                          dependency.getType(),
97                          dependency.getScope(),
98                          dependency.getClassifier());
99              } catch (InvalidVersionSpecificationException e) {
100                 throw new IllegalArgumentException(e);
101             }
102         }
103 
104         private boolean match(
105                 String groupId, String artifactId, String version, String type, String scope, String classifier)
106                 throws InvalidVersionSpecificationException {
107             switch (parts.length) {
108                 case 6:
109                     if (!matches(5, classifier)) {
110                         return false;
111                     }
112                 case 5:
113                     if (scope == null || scope.isEmpty()) {
114                         scope = Artifact.SCOPE_COMPILE;
115                     }
116 
117                     if (!matches(4, scope)) {
118                         return false;
119                     }
120                 case 4:
121                     if (type == null || type.isEmpty()) {
122                         type = "jar";
123                     }
124 
125                     if (!matches(3, type)) {
126                         return false;
127                     }
128 
129                 case 3:
130                     if (!matches(2, version)) {
131                         if (!containsVersion(
132                                 VersionRange.createFromVersionSpec(parts[2]), new DefaultArtifactVersion(version))) {
133                             return false;
134                         }
135                     }
136 
137                 case 2:
138                     if (!matches(1, artifactId)) {
139                         return false;
140                     }
141                 case 1:
142                     return matches(0, groupId);
143                 default:
144                     throw new AssertionError();
145             }
146         }
147 
148         private boolean matches(int index, String input) {
149             // TODO: Check if this can be done better or prevented earlier.
150             if (input == null) {
151                 input = "";
152             }
153             if (partsRegex[index] == null) {
154                 String regex = parts[index]
155                         .replace(".", "\\.")
156                         .replace("*", ".*")
157                         .replace(":", "\\:")
158                         .replace('?', '.')
159                         .replace("[", "\\[")
160                         .replace("]", "\\]")
161                         .replace("(", "\\(")
162                         .replace(")", "\\)");
163 
164                 if (".*".equals(regex)) {
165                     partsRegex[index] = test -> true;
166                 } else {
167                     partsRegex[index] = test ->
168                             java.util.regex.Pattern.compile(regex).matcher(test).matches();
169                 }
170             }
171             return partsRegex[index].test(input);
172         }
173 
174         @Override
175         public String toString() {
176             return pattern;
177         }
178     }
179 
180     private final Collection<Pattern> excludePatterns = new HashSet<>();
181 
182     private final Collection<Pattern> includePatterns = new HashSet<>();
183 
184     /**
185      * Construct class by providing patterns as strings. Empty strings are ignored.
186      *
187      * @param excludeStrings includes
188      * @param includeStrings excludes
189      * @throws NullPointerException if any of the arguments is null
190      */
191     public ArtifactMatcher(final Collection<String> excludeStrings, final Collection<String> includeStrings) {
192         ofNullable(excludeStrings).ifPresent(excludes -> excludes.stream()
193                 .filter(StringUtils::isNotEmpty)
194                 .map(Pattern::new)
195                 .forEach(excludePatterns::add));
196         ofNullable(includeStrings).ifPresent(includes -> includes.stream()
197                 .filter(StringUtils::isNotEmpty)
198                 .map(Pattern::new)
199                 .forEach(includePatterns::add));
200     }
201 
202     private boolean match(Function<Pattern, Boolean> matcher) {
203         return excludePatterns.stream().anyMatch(matcher::apply)
204                 && includePatterns.stream().noneMatch(matcher::apply);
205     }
206 
207     /**
208      * Check if artifact matches patterns.
209      *
210      * @param artifact the artifact to match
211      * @return {@code true} if artifact matches any {@link #excludePatterns} and none of the {@link #includePatterns}, otherwise
212      *         {@code false}
213      */
214     public boolean match(Artifact artifact) {
215         return match(p -> p.match(artifact));
216     }
217 
218     /**
219      * Check if dependency matches patterns.
220      *
221      * @param dependency the dependency to match
222      * @return {@code true} if dependency matches any {@link #excludePatterns} and none of the {@link #includePatterns},
223      *         otherwise {@code false}
224      */
225     public boolean match(Dependency dependency) {
226         return match(p -> p.match(dependency));
227     }
228 
229     /**
230      * Copied from Artifact.VersionRange. This is tweaked to handle singular ranges properly. The default
231      * containsVersion method assumes a singular version means allow everything.
232      * This method assumes that "2.0.4" == "[2.0.4,)"
233      *
234      * @param allowedRange range of allowed versions
235      * @param version the version to be checked
236      * @return true if the version is contained by the range
237      */
238     public static boolean containsVersion(VersionRange allowedRange, ArtifactVersion version) {
239         ArtifactVersion recommendedVersion = allowedRange.getRecommendedVersion();
240         if (recommendedVersion == null) {
241             return allowedRange.containsVersion(version);
242         } else {
243             // only singular versions ever have a recommendedVersion
244             int compareTo = recommendedVersion.compareTo(version);
245             return compareTo <= 0;
246         }
247     }
248 
249     /**
250      * To be used for artifacts which are equivalent for the purposes of the {@link ArtifactMatcher}.
251      */
252     public static class MatchingArtifact {
253         String artifactString;
254 
255         public MatchingArtifact(Artifact artifact) {
256             artifactString = new StringBuilder()
257                     .append(artifact.getGroupId())
258                     .append(":")
259                     .append(artifact.getArtifactId())
260                     .append(":")
261                     .append(artifact.getVersion())
262                     .append(":")
263                     .append(artifact.getType())
264                     .append(":")
265                     .append(artifact.getScope())
266                     .append(":")
267                     .append(artifact.getClassifier())
268                     .toString();
269         }
270 
271         @Override
272         public int hashCode() {
273             return artifactString.hashCode();
274         }
275 
276         @Override
277         public boolean equals(Object obj) {
278             if (this == obj) {
279                 return true;
280             }
281             if (obj == null) {
282                 return false;
283             }
284             if (getClass() != obj.getClass()) {
285                 return false;
286             }
287             MatchingArtifact other = (MatchingArtifact) obj;
288             return Objects.equals(artifactString, other.artifactString);
289         }
290 
291         @Override
292         public String toString() {
293             return artifactString;
294         }
295     }
296 }