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