001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.maven.enforcer.rules.utils; 020 021import java.util.Collection; 022import java.util.HashSet; 023import java.util.Objects; 024import java.util.function.Function; 025import java.util.function.Predicate; 026 027import org.apache.maven.artifact.Artifact; 028import org.apache.maven.artifact.versioning.ArtifactVersion; 029import org.apache.maven.artifact.versioning.DefaultArtifactVersion; 030import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException; 031import org.apache.maven.artifact.versioning.VersionRange; 032import org.apache.maven.model.Dependency; 033import org.codehaus.plexus.util.StringUtils; 034 035import static java.util.Optional.ofNullable; 036 037/** 038 * This class is used for matching Artifacts against a list of patterns. 039 * 040 * @author Jakub Senko 041 */ 042public final class ArtifactMatcher { 043 044 /** 045 * @author I don't know 046 */ 047 public static class Pattern { 048 private final String pattern; 049 050 private final String[] parts; 051 private final Predicate<String>[] partsRegex; 052 053 public Pattern(String pattern) { 054 if (pattern == null) { 055 throw new NullPointerException("pattern"); 056 } 057 058 this.pattern = pattern; 059 060 parts = pattern.split(":", 7); 061 062 if (parts.length == 7) { 063 throw new IllegalArgumentException("Pattern contains too many delimiters."); 064 } 065 066 for (String part : parts) { 067 if ("".equals(part)) { 068 throw new IllegalArgumentException("Pattern or its part is empty."); 069 } 070 } 071 partsRegex = new Predicate[parts.length]; 072 } 073 074 public boolean match(Artifact artifact) { 075 Objects.requireNonNull(artifact, "artifact must not be null"); 076 try { 077 return match( 078 artifact.getGroupId(), 079 artifact.getArtifactId(), 080 artifact.getVersion(), 081 artifact.getType(), 082 artifact.getScope(), 083 artifact.getClassifier()); 084 } catch (InvalidVersionSpecificationException e) { 085 throw new IllegalArgumentException(e); 086 } 087 } 088 089 public boolean match(Dependency dependency) { 090 Objects.requireNonNull(dependency, "dependency must not be null"); 091 try { 092 return match( 093 dependency.getGroupId(), 094 dependency.getArtifactId(), 095 dependency.getVersion(), 096 dependency.getType(), 097 dependency.getScope(), 098 dependency.getClassifier()); 099 } 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) 193 .ifPresent(excludes -> excludes.stream() 194 .filter(StringUtils::isNotEmpty) 195 .map(Pattern::new) 196 .forEach(excludePatterns::add)); 197 ofNullable(includeStrings) 198 .ifPresent(includes -> includes.stream() 199 .filter(StringUtils::isNotEmpty) 200 .map(Pattern::new) 201 .forEach(includePatterns::add)); 202 } 203 204 private boolean match(Function<Pattern, Boolean> matcher) { 205 return excludePatterns.stream().anyMatch(matcher::apply) 206 && includePatterns.stream().noneMatch(matcher::apply); 207 } 208 209 /** 210 * Check if artifact matches patterns. 211 * 212 * @param artifact the artifact to match 213 * @return {@code true} if artifact matches any {@link #excludePatterns} and none of the {@link #includePatterns}, otherwise 214 * {@code false} 215 */ 216 public boolean match(Artifact artifact) { 217 return match(p -> p.match(artifact)); 218 } 219 220 /** 221 * Check if dependency matches patterns. 222 * 223 * @param dependency the dependency to match 224 * @return {@code true} if dependency matches any {@link #excludePatterns} and none of the {@link #includePatterns}, 225 * otherwise {@code false} 226 */ 227 public boolean match(Dependency dependency) { 228 return match(p -> p.match(dependency)); 229 } 230 231 /** 232 * Copied from Artifact.VersionRange. This is tweaked to handle singular ranges properly. The default 233 * containsVersion method assumes a singular version means allow everything. 234 * This method assumes that "2.0.4" == "[2.0.4,)" 235 * 236 * @param allowedRange range of allowed versions 237 * @param version the version to be checked 238 * @return true if the version is contained by the range 239 */ 240 public static boolean containsVersion(VersionRange allowedRange, ArtifactVersion version) { 241 ArtifactVersion recommendedVersion = allowedRange.getRecommendedVersion(); 242 if (recommendedVersion == null) { 243 return allowedRange.containsVersion(version); 244 } else { 245 // only singular versions ever have a recommendedVersion 246 int compareTo = recommendedVersion.compareTo(version); 247 return compareTo <= 0; 248 } 249 } 250 251 /** 252 * To be used for artifacts which are equivalent for the purposes of the {@link ArtifactMatcher}. 253 */ 254 public static class MatchingArtifact { 255 String artifactString; 256 257 public MatchingArtifact(Artifact artifact) { 258 artifactString = new StringBuilder() 259 .append(artifact.getGroupId()) 260 .append(":") 261 .append(artifact.getArtifactId()) 262 .append(":") 263 .append(artifact.getVersion()) 264 .append(":") 265 .append(artifact.getType()) 266 .append(":") 267 .append(artifact.getScope()) 268 .append(":") 269 .append(artifact.getClassifier()) 270 .toString(); 271 } 272 273 @Override 274 public int hashCode() { 275 return artifactString.hashCode(); 276 } 277 278 @Override 279 public boolean equals(Object obj) { 280 if (this == obj) { 281 return true; 282 } 283 if (obj == null) { 284 return false; 285 } 286 if (getClass() != obj.getClass()) { 287 return false; 288 } 289 MatchingArtifact other = (MatchingArtifact) obj; 290 return Objects.equals(artifactString, other.artifactString); 291 } 292 293 @Override 294 public String toString() { 295 return artifactString; 296 } 297 } 298}