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}