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.util.graph.transformer;
020
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.HashSet;
024import java.util.Iterator;
025import java.util.Objects;
026import java.util.Set;
027import java.util.stream.Collectors;
028
029import org.eclipse.aether.RepositoryException;
030import org.eclipse.aether.collection.UnsolvableVersionConflictException;
031import org.eclipse.aether.graph.DependencyFilter;
032import org.eclipse.aether.graph.DependencyNode;
033import org.eclipse.aether.util.graph.transformer.ConflictResolver.ConflictContext;
034import org.eclipse.aether.util.graph.transformer.ConflictResolver.ConflictItem;
035import org.eclipse.aether.util.graph.transformer.ConflictResolver.VersionSelector;
036import org.eclipse.aether.util.graph.visitor.PathRecordingDependencyVisitor;
037import org.eclipse.aether.util.graph.visitor.TreeDependencyVisitor;
038import org.eclipse.aether.version.Version;
039import org.eclipse.aether.version.VersionConstraint;
040
041import static java.util.Objects.requireNonNull;
042
043/**
044 * A configurable version selector for use with {@link ConflictResolver} that resolves version conflicts using a
045 * specified strategy. If there is no single node that satisfies all encountered version ranges, the selector will fail.
046 * Based on configuration, this selector may fail for other reasons as well.
047 *
048 * @since 2.0.0
049 */
050public class ConfigurableVersionSelector extends VersionSelector {
051    /**
052     * The strategy how "winner" is being selected.
053     */
054    public interface SelectionStrategy {
055        /**
056         * Invoked for every "candidate" when winner is already set (very first candidate is set as winner).
057         * <p>
058         * This method should determine is candidate "better" or not and should replace current winner. This method
059         * is invoked whenever {@code candidate} is "considered" (fits any constraint in effect, if any).
060         */
061        boolean isBetter(ConflictItem candidate, ConflictItem winner);
062        /**
063         * Method invoked at version selection end, just before version selector returns. Note: {@code winner} may
064         * be {@code null}, while the rest of parameters cannot. The parameter {@code candidates} contains all the
065         * "considered candidates", dependencies that fulfil all constraints, if present. The {@code context} on the
066         * other hand contains all items participating in conflict.
067         * <p>
068         * This method by default just returns the passed in {@code winner}, but can do much more.
069         */
070        default ConflictItem winnerSelected(
071                ConflictItem winner, Collection<ConflictItem> candidates, ConflictContext context)
072                throws UnsolvableVersionConflictException {
073            return winner;
074        }
075    }
076    /**
077     * The strategy of winner selection, never {@code null}.
078     */
079    protected final SelectionStrategy selectionStrategy;
080
081    /**
082     * Creates a new instance of this version selector that works "as Maven did so far".
083     */
084    public ConfigurableVersionSelector() {
085        this(new Nearest());
086    }
087
088    /**
089     * Creates a new instance of this version selector.
090     *
091     * @param selectionStrategy The winner selection strategy, must not be {@code null}. Maven3
092     *                          used {@link Nearest} strategy.
093     */
094    public ConfigurableVersionSelector(SelectionStrategy selectionStrategy) {
095        this.selectionStrategy = requireNonNull(selectionStrategy, "selectionStrategy");
096    }
097
098    @Override
099    public void selectVersion(ConflictContext context) throws RepositoryException {
100        ConflictGroup group = new ConflictGroup();
101        for (ConflictItem candidate : context.getItems()) {
102            DependencyNode node = candidate.getNode();
103            VersionConstraint constraint = node.getVersionConstraint();
104
105            boolean backtrack = false;
106            boolean hardConstraint = constraint.getRange() != null;
107
108            if (hardConstraint) {
109                if (group.constraints.add(constraint)) {
110                    if (group.winner != null
111                            && !constraint.containsVersion(
112                                    group.winner.getNode().getVersion())) {
113                        backtrack = true;
114                    }
115                }
116            }
117
118            if (isAcceptableByConstraints(group, node.getVersion())) {
119                group.candidates.add(candidate);
120
121                if (backtrack) {
122                    backtrack(group, context);
123                } else if (group.winner == null || selectionStrategy.isBetter(candidate, group.winner)) {
124                    group.winner = candidate;
125                }
126            } else if (backtrack) {
127                backtrack(group, context);
128            }
129        }
130        context.setWinner(selectionStrategy.winnerSelected(group.winner, group.candidates, context));
131    }
132
133    protected void backtrack(ConflictGroup group, ConflictContext context) throws UnsolvableVersionConflictException {
134        group.winner = null;
135
136        for (Iterator<ConflictItem> it = group.candidates.iterator(); it.hasNext(); ) {
137            ConflictItem candidate = it.next();
138
139            if (!isAcceptableByConstraints(group, candidate.getNode().getVersion())) {
140                it.remove();
141            } else if (group.winner == null || selectionStrategy.isBetter(candidate, group.winner)) {
142                group.winner = candidate;
143            }
144        }
145
146        if (group.winner == null) {
147            throw newFailure("Unsolvable hard constraint combination", context);
148        }
149    }
150
151    protected boolean isAcceptableByConstraints(ConflictGroup group, Version version) {
152        for (VersionConstraint constraint : group.constraints) {
153            if (!constraint.containsVersion(version)) {
154                return false;
155            }
156        }
157        return true;
158    }
159
160    /**
161     * Helper method to create failure, creates instance of {@link UnsolvableVersionConflictException}.
162     */
163    public static UnsolvableVersionConflictException newFailure(String message, ConflictContext context) {
164        DependencyFilter filter = (node, parents) -> {
165            requireNonNull(node, "node cannot be null");
166            requireNonNull(parents, "parents cannot be null");
167            return context.isIncluded(node);
168        };
169        PathRecordingDependencyVisitor visitor = new PathRecordingDependencyVisitor(filter);
170        context.getRoot().accept(new TreeDependencyVisitor(visitor));
171        return new UnsolvableVersionConflictException(message, visitor.getPaths());
172    }
173
174    protected static class ConflictGroup {
175
176        final Collection<VersionConstraint> constraints;
177
178        final Collection<ConflictItem> candidates;
179
180        ConflictItem winner;
181
182        ConflictGroup() {
183            constraints = new HashSet<>();
184            candidates = new ArrayList<>(64);
185        }
186
187        @Override
188        public String toString() {
189            return String.valueOf(winner);
190        }
191    }
192
193    /**
194     * Selection strategy that selects "nearest" (to the root) version.
195     * <p>
196     * This is the "classic" Maven strategy.
197     */
198    public static class Nearest implements SelectionStrategy {
199        @Override
200        public boolean isBetter(ConflictItem candidate, ConflictItem winner) {
201            if (candidate.isSibling(winner)) {
202                return candidate
203                                .getNode()
204                                .getVersion()
205                                .compareTo(winner.getNode().getVersion())
206                        > 0;
207            } else {
208                return candidate.getDepth() < winner.getDepth();
209            }
210        }
211    }
212
213    /**
214     * Selection strategy that selects "highest" version.
215     */
216    public static class Highest implements SelectionStrategy {
217        @Override
218        public boolean isBetter(ConflictItem candidate, ConflictItem winner) {
219            return candidate.getNode().getVersion().compareTo(winner.getNode().getVersion()) > 0;
220        }
221    }
222
223    /**
224     * Example selection strategy (used in tests and demos), is not recommended to be used in production.
225     * <p>
226     * Selection strategy that delegates to another selection strategy, and at the end enforces dependency convergence
227     * among candidates.
228     */
229    public static class VersionConvergence implements SelectionStrategy {
230        private final SelectionStrategy delegate;
231
232        public VersionConvergence(SelectionStrategy delegate) {
233            this.delegate = requireNonNull(delegate, "delegate");
234        }
235
236        @Override
237        public boolean isBetter(ConflictItem candidate, ConflictItem winner) {
238            return delegate.isBetter(candidate, winner);
239        }
240
241        @Override
242        public ConflictItem winnerSelected(
243                ConflictItem winner, Collection<ConflictItem> candidates, ConflictContext context)
244                throws UnsolvableVersionConflictException {
245            if (winner != null && winner.getNode().getVersionConstraint().getRange() == null) {
246                Set<String> versions = candidates.stream()
247                        .map(c -> c.getDependency().getArtifact().getVersion())
248                        .collect(Collectors.toSet());
249                if (versions.size() > 1) {
250                    throw newFailure(
251                            "Convergence violated for "
252                                    + winner.getDependency().getArtifact().getGroupId() + ":"
253                                    + winner.getDependency().getArtifact().getArtifactId() + ", versions present: "
254                                    + versions,
255                            context);
256                }
257            }
258            return winner;
259        }
260    }
261
262    /**
263     * Example selection strategy (used in tests and demos), is not recommended to be used in production.
264     * <p>
265     * Selection strategy that delegates to another selection strategy, and at end enforces aligned "major versions"
266     * among candidates.
267     */
268    public static class MajorVersionConvergence implements SelectionStrategy {
269        private final SelectionStrategy delegate;
270
271        public MajorVersionConvergence(SelectionStrategy delegate) {
272            this.delegate = requireNonNull(delegate, "delegate");
273        }
274
275        @Override
276        public boolean isBetter(ConflictItem candidate, ConflictItem winner) {
277            return delegate.isBetter(candidate, winner);
278        }
279
280        @Override
281        public ConflictItem winnerSelected(
282                ConflictItem winner, Collection<ConflictItem> candidates, ConflictContext context)
283                throws UnsolvableVersionConflictException {
284            if (winner != null && !candidates.isEmpty()) {
285                Set<String> incompatibleVersions = candidates.stream()
286                        .filter(c -> !sameMajor(c, winner))
287                        .map(c -> c.getDependency().getArtifact().getVersion())
288                        .collect(Collectors.toSet());
289                if (!incompatibleVersions.isEmpty()) {
290                    Set<String> allVersions = candidates.stream()
291                            .map(c -> c.getDependency().getArtifact().getVersion())
292                            .collect(Collectors.toSet());
293                    throw newFailure(
294                            "Incompatible versions for "
295                                    + winner.getDependency().getArtifact().getGroupId() + ":"
296                                    + winner.getDependency().getArtifact().getArtifactId() + ", incompatible versions: "
297                                    + incompatibleVersions + ", all versions " + allVersions,
298                            context);
299                }
300            }
301            return winner;
302        }
303
304        private boolean sameMajor(ConflictItem candidate, ConflictItem winner) {
305            String candidateVersion = candidate.getDependency().getArtifact().getVersion();
306            String winnerVersion = winner.getDependency().getArtifact().getVersion();
307            // for now a naive check: major versions should be same
308            if (candidateVersion.contains(".") && winnerVersion.contains(".")) {
309                String candidateMajor = candidateVersion.substring(0, candidateVersion.indexOf('.'));
310                String winnerMajor = winnerVersion.substring(0, winnerVersion.indexOf('.'));
311                return Objects.equals(candidateMajor, winnerMajor);
312            }
313            return true; // cannot determine, so just leave it
314        }
315    }
316}