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}