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.eclipse.aether.internal.impl.collect;
020
021import javax.inject.Named;
022import javax.inject.Singleton;
023
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.List;
027import java.util.Optional;
028import java.util.function.Function;
029import java.util.function.Predicate;
030import java.util.stream.Collectors;
031
032import org.eclipse.aether.artifact.Artifact;
033import org.eclipse.aether.collection.VersionFilter;
034import org.eclipse.aether.collection.VersionFilterBuilder;
035import org.eclipse.aether.util.ConfigUtils;
036import org.eclipse.aether.util.graph.version.ChainedVersionFilter;
037import org.eclipse.aether.util.graph.version.ContextPredicateDelegatingVersionFilter;
038import org.eclipse.aether.util.graph.version.ContextualSnapshotVersionFilter;
039import org.eclipse.aether.util.graph.version.GenericQualifiersVersionFilter;
040import org.eclipse.aether.util.graph.version.HighestVersionFilter;
041import org.eclipse.aether.util.graph.version.LowestVersionFilter;
042import org.eclipse.aether.util.graph.version.ReleaseVersionFilter;
043import org.eclipse.aether.util.graph.version.SnapshotVersionFilter;
044import org.eclipse.aether.util.graph.version.VersionPredicateVersionFilter;
045import org.eclipse.aether.version.VersionConstraint;
046
047import static java.util.Objects.requireNonNull;
048
049/**
050 * Builds {@link VersionFilter} instances out of input expression string.
051 *
052 * Expression is a semicolon separated list of filters to apply. By default, no version filter is applied (like in Maven 3).
053 * <br/>
054 * Supported filters:
055 * <ul>
056 *     <li>{@code "s"} - contextual snapshot filter (project version decides are snapshots allowed or not)</li>
057 *     <li>{@code "nosnapshot"} - unconditional snapshot filter (no snapshot versions selected from ranges)</li>
058 *     <li>{@code "norelease"} - unconditional release filter (no release versions selected from ranges)</li>
059 *     <li>{@code "nopreview"} - unconditional preview filter (no preview versions selected from ranges)</li>
060 *     <li>{@code "noprerelease"} - unconditional pre-release filter (no preview and rc/cr versions selected from ranges)</li>
061 *     <li>{@code "noqualifier"} - unconditional any-qualifier filter (no version with any qualifier selected from ranges)</li>
062 *     <li>{@code "h"} (shorthand of {@code h(1)}) or {@code "h(num)"} - highest N version (based on version ordering)</li>
063 *     <li>{@code "l"} (shorthand of {@code l(1)}) or {@code "l(num)"} - lowest N version (based on version ordering)</li>
064 *     <li>{@code "e(V)"} - exclusion filter (excludes versions matching V version constraint)</li>
065 *     <li>{@code "i(V)"} - inclusion filter (includes versions matching V version constraint)</li>
066 * </ul>
067 * Every filter expression may have "scope" applied, in form of {@code @G[:A]}. Presence of "scope" narrows the
068 * application of filter to given G or G:A.
069 * <p>
070 * In case of multiple "similar" rule scopes, user should enlist rules from "most specific" to "least specific".
071 * <p>
072 * Example filter expression: <code>"h(5);s;e(1)@org.foo:bar</code> will cause:
073 * <ul>
074 *     <li>ranges are filtered for "top 5" (instead of full range)</li>
075 *     <li>snapshots are banned if root project is not a snapshot</li>
076 *     <li>if range for <code>org.foo:bar</code> is being processed, version 1 is omitted</li>
077 * </ul>
078 * Values in this property builds <code>org.eclipse.aether.collection.VersionFilter</code> instance.
079 *
080 * @since 2.0.18
081 */
082@Singleton
083@Named
084public class DefaultVersionFilterBuilder implements VersionFilterBuilder {
085    /**
086     * Builds a version filter based on the given filter expression.
087     *
088     * @param filterExpression a string containing filter expressions, may be {@code null}.
089     * @param versionConstraintParser version constraint parts to be used during parsing, must not be {@code null}.
090     * @return optional version filter, never {@code null}.
091     */
092    @Override
093    public Optional<VersionFilter> buildVersionFilter(
094            String filterExpression, Function<String, VersionConstraint> versionConstraintParser) {
095        requireNonNull(versionConstraintParser);
096        ArrayList<VersionFilter> filters = new ArrayList<>();
097        if (filterExpression != null) {
098            List<String> expressions = Arrays.stream(filterExpression.split(";"))
099                    .filter(s -> !s.trim().isEmpty())
100                    .collect(Collectors.toList());
101            for (String expression : expressions) {
102                Predicate<Artifact> scopePredicate;
103                VersionFilter filter;
104                if (expression.contains("@")) {
105                    String remainder = expression.substring(expression.indexOf('@') + 1);
106                    if (remainder.contains(":")) {
107                        String g = remainder.substring(0, remainder.indexOf(':'));
108                        String a = remainder.substring(remainder.indexOf(':') + 1);
109                        scopePredicate =
110                                artifact -> g.equals(artifact.getGroupId()) && a.equals(artifact.getArtifactId());
111                    } else {
112                        scopePredicate = artifact -> remainder.equals(artifact.getGroupId());
113                    }
114                    expression = expression.substring(0, expression.indexOf('@'));
115                } else {
116                    scopePredicate = null;
117                }
118                if ("s".equals(expression)) {
119                    filter = new ContextualSnapshotVersionFilter();
120                } else if ("nosnapshot".equals(expression)) {
121                    filter = new SnapshotVersionFilter();
122                } else if ("norelease".equals(expression)) {
123                    filter = new ReleaseVersionFilter();
124                } else if ("nopreview".equals(expression)) {
125                    filter = GenericQualifiersVersionFilter.previewVersionFilter();
126                } else if ("noprerelease".equals(expression)) {
127                    filter = GenericQualifiersVersionFilter.preReleaseVersionFilter();
128                } else if ("noqualifier".equals(expression)) {
129                    filter = GenericQualifiersVersionFilter.anyQualifierVersionFilter();
130                } else if ("h".equals(expression)) {
131                    filter = new HighestVersionFilter();
132                } else if ("l".equals(expression)) {
133                    filter = new LowestVersionFilter();
134                } else if ((expression.startsWith("h(") || expression.startsWith("l(")) && expression.endsWith(")")) {
135                    int num = Integer.parseInt(expression.substring(2, expression.length() - 1));
136                    if (expression.startsWith("h(")) {
137                        filter = new HighestVersionFilter(num);
138                    } else {
139                        filter = new LowestVersionFilter(num);
140                    }
141                } else if ((expression.startsWith("e(") || (expression.startsWith("i("))) && expression.endsWith(")")) {
142                    VersionConstraint versionConstraint =
143                            versionConstraintParser.apply(expression.substring(2, expression.length() - 1));
144                    if (expression.startsWith("e(")) {
145                        // exclude
146                        filter = new VersionPredicateVersionFilter(v -> !versionConstraint.containsVersion(v));
147                    } else {
148                        // include
149                        filter = new VersionPredicateVersionFilter(versionConstraint::containsVersion);
150                    }
151                } else {
152                    throw new IllegalArgumentException("Unsupported filter expression: " + expression);
153                }
154
155                filters.add(contextPredicate(scopePredicate, filter));
156            }
157        }
158        if (filters.isEmpty()) {
159            return Optional.empty();
160        } else if (filters.size() == 1) {
161            return Optional.of(filters.get(0));
162        } else {
163            return Optional.of(ChainedVersionFilter.newInstance(filters));
164        }
165    }
166
167    private VersionFilter contextPredicate(Predicate<Artifact> artifactPredicate, VersionFilter filter) {
168        Predicate<VersionFilter.VersionFilterContext> contextPredicate =
169                c -> !ConfigUtils.getBoolean(c.getSession(), false, VERSION_FILTER_SUPPRESSED);
170        if (artifactPredicate != null) {
171            contextPredicate = contextPredicate.and(
172                    c -> artifactPredicate.test(c.getDependency().getArtifact()));
173        }
174        return new ContextPredicateDelegatingVersionFilter(contextPredicate, filter);
175    }
176}