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