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 String pattern; 048 049 private String[] parts; 050 051 public Pattern(String pattern) { 052 if (pattern == null) { 053 throw new NullPointerException("pattern"); 054 } 055 056 this.pattern = pattern; 057 058 parts = pattern.split(":", 7); 059 060 if (parts.length == 7) { 061 throw new IllegalArgumentException("Pattern contains too many delimiters."); 062 } 063 064 for (String part : parts) { 065 if ("".equals(part)) { 066 throw new IllegalArgumentException("Pattern or its part is empty."); 067 } 068 } 069 } 070 071 public boolean match(Artifact artifact) { 072 Objects.requireNonNull(artifact, "artifact must not be null"); 073 try { 074 return match( 075 artifact.getGroupId(), 076 artifact.getArtifactId(), 077 artifact.getVersion(), 078 artifact.getType(), 079 artifact.getScope(), 080 artifact.getClassifier()); 081 } catch (InvalidVersionSpecificationException e) { 082 throw new IllegalArgumentException(e); 083 } 084 } 085 086 public boolean match(Dependency dependency) { 087 Objects.requireNonNull(dependency, "dependency must not be null"); 088 try { 089 return match( 090 dependency.getGroupId(), 091 dependency.getArtifactId(), 092 dependency.getVersion(), 093 dependency.getType(), 094 dependency.getScope(), 095 dependency.getClassifier()); 096 } catch (InvalidVersionSpecificationException e) { 097 throw new IllegalArgumentException(e); 098 } 099 } 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}