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}