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}