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; 025 026import org.apache.maven.artifact.Artifact; 027import org.apache.maven.artifact.versioning.ArtifactVersion; 028import org.apache.maven.artifact.versioning.DefaultArtifactVersion; 029import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException; 030import org.apache.maven.artifact.versioning.VersionRange; 031import org.apache.maven.model.Dependency; 032import org.codehaus.plexus.util.StringUtils; 033 034import static java.util.Optional.ofNullable; 035 036/** 037 * This class is used for matching Artifacts against a list of patterns. 038 * 039 * @author Jakub Senko 040 */ 041public final class ArtifactMatcher { 042 043 /** 044 * @author I don't know 045 */ 046 public static class Pattern { 047 private final String pattern; 048 049 private final String[] parts; 050 private final java.util.regex.Pattern[] partsRegex; 051 052 public Pattern(String pattern) { 053 if (pattern == null) { 054 throw new NullPointerException("pattern"); 055 } 056 057 this.pattern = pattern; 058 059 parts = pattern.split(":", 7); 060 061 if (parts.length == 7) { 062 throw new IllegalArgumentException("Pattern contains too many delimiters."); 063 } 064 065 for (String part : parts) { 066 if ("".equals(part)) { 067 throw new IllegalArgumentException("Pattern or its part is empty."); 068 } 069 } 070 partsRegex = new java.util.regex.Pattern[parts.length]; 071 } 072 073 public boolean match(Artifact artifact) { 074 Objects.requireNonNull(artifact, "artifact must not be null"); 075 try { 076 return match( 077 artifact.getGroupId(), 078 artifact.getArtifactId(), 079 artifact.getVersion(), 080 artifact.getType(), 081 artifact.getScope(), 082 artifact.getClassifier()); 083 } catch (InvalidVersionSpecificationException e) { 084 throw new IllegalArgumentException(e); 085 } 086 } 087 088 public boolean match(Dependency dependency) { 089 Objects.requireNonNull(dependency, "dependency must not be null"); 090 try { 091 return match( 092 dependency.getGroupId(), 093 dependency.getArtifactId(), 094 dependency.getVersion(), 095 dependency.getType(), 096 dependency.getScope(), 097 dependency.getClassifier()); 098 } catch (InvalidVersionSpecificationException e) { 099 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}