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).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}