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.*;
022
023import org.eclipse.aether.RepositoryException;
024import org.eclipse.aether.RepositorySystemSession;
025import org.eclipse.aether.artifact.Artifact;
026import org.eclipse.aether.collection.DependencyGraphTransformationContext;
027import org.eclipse.aether.collection.DependencyGraphTransformer;
028import org.eclipse.aether.graph.DefaultDependencyNode;
029import org.eclipse.aether.graph.Dependency;
030import org.eclipse.aether.graph.DependencyNode;
031import org.eclipse.aether.util.artifact.ArtifactIdUtils;
032
033import static java.util.Objects.requireNonNull;
034
035/**
036 * A dependency graph transformer that resolves version and scope conflicts among dependencies. For a given set of
037 * conflicting nodes, one node will be chosen as the winner and the other nodes are removed from the dependency graph.
038 * The exact rules by which a winning node and its effective scope are determined are controlled by user-supplied
039 * implementations of {@link VersionSelector}, {@link ScopeSelector}, {@link OptionalitySelector} and
040 * {@link ScopeDeriver}.
041 * <p>
042 * By default, this graph transformer will turn the dependency graph into a tree without duplicate artifacts. Using the
043 * configuration property {@link #CONFIG_PROP_VERBOSE}, a verbose mode can be enabled where the graph is still turned
044 * into a tree but all nodes participating in a conflict are retained. The nodes that were rejected during conflict
045 * resolution have no children and link back to the winner node via the {@link #NODE_DATA_WINNER} key in their custom
046 * data. Additionally, the keys {@link #NODE_DATA_ORIGINAL_SCOPE} and {@link #NODE_DATA_ORIGINAL_OPTIONALITY} are used
047 * to store the original scope and optionality of each node. Obviously, the resulting dependency tree is not suitable
048 * for artifact resolution unless a filter is employed to exclude the duplicate dependencies.
049 * <p>
050 * This transformer will query the keys {@link TransformationContextKeys#CONFLICT_IDS},
051 * {@link TransformationContextKeys#SORTED_CONFLICT_IDS}, {@link TransformationContextKeys#CYCLIC_CONFLICT_IDS} for
052 * existing information about conflict ids. In absence of this information, it will automatically invoke the
053 * {@link ConflictIdSorter} to calculate it.
054 */
055public final class ConflictResolver implements DependencyGraphTransformer {
056
057    /**
058     * The key in the repository session's {@link org.eclipse.aether.RepositorySystemSession#getConfigProperties()
059     * configuration properties} used to store a {@link Boolean} flag controlling the transformer's verbose mode.
060     * Accepted values are {@link Boolean} type, {@link String} type (where "true" would be interpreted as {@code true}
061     * or {@link Verbosity} enum instances.
062     */
063    public static final String CONFIG_PROP_VERBOSE = "aether.conflictResolver.verbose";
064
065    /**
066     * The enum representing verbosity levels of conflict resolver.
067     *
068     * @since 1.9.8
069     */
070    public enum Verbosity {
071        /**
072         * Verbosity level to be used in all "common" resolving use cases (ie. dependencies to build class path). The
073         * {@link ConflictResolver} in this mode will trim down the graph to the barest minimum: will not leave
074         * any conflicting node in place, hence no conflicts will be present in transformed graph either.
075         */
076        NONE,
077
078        /**
079         * Verbosity level to be used in "analyze" resolving use cases (ie. dependency convergence calculations). The
080         * {@link ConflictResolver} in this mode will remove any redundant collected nodes, in turn it will leave one
081         * with recorded conflicting information. This mode corresponds to "classic verbose" mode when
082         * {@link #CONFIG_PROP_VERBOSE} was set to {@code true}. Obviously, the resulting dependency tree is not
083         * suitable for artifact resolution unless a filter is employed to exclude the duplicate dependencies.
084         */
085        STANDARD,
086
087        /**
088         * Verbosity level to be used in "analyze" resolving use cases (ie. dependency convergence calculations). The
089         * {@link ConflictResolver} in this mode will not remove any collected node, in turn it will record on all
090         * eliminated nodes the conflicting information. Obviously, the resulting dependency tree is not suitable
091         * for artifact resolution unless a filter is employed to exclude the duplicate dependencies.
092         */
093        FULL
094    }
095
096    /**
097     * Helper method that uses {@link RepositorySystemSession} and {@link #CONFIG_PROP_VERBOSE} key to figure out
098     * current {@link Verbosity}: if {@link Boolean} or {@code String} found, returns {@link Verbosity#STANDARD}
099     * or {@link Verbosity#NONE}, depending on value (string is parsed with {@link Boolean#parseBoolean(String)}
100     * for {@code true} or {@code false} correspondingly. This is to retain "existing" behavior, where the config
101     * key accepted only these values.
102     * Since 1.9.8 release, this key may contain {@link Verbosity} enum instance as well, in which case that instance
103     * is returned.
104     * This method never returns {@code null}.
105     */
106    private static Verbosity getVerbosity(RepositorySystemSession session) {
107        final Object verbosityValue = session.getConfigProperties().get(CONFIG_PROP_VERBOSE);
108        if (verbosityValue instanceof Boolean) {
109            return (Boolean) verbosityValue ? Verbosity.STANDARD : Verbosity.NONE;
110        } else if (verbosityValue instanceof String) {
111            return Boolean.parseBoolean(verbosityValue.toString()) ? Verbosity.STANDARD : Verbosity.NONE;
112        } else if (verbosityValue instanceof Verbosity) {
113            return (Verbosity) verbosityValue;
114        } else if (verbosityValue != null) {
115            throw new IllegalArgumentException("Unsupported Verbosity configuration: " + verbosityValue);
116        }
117        return Verbosity.NONE;
118    }
119
120    /**
121     * The key in the dependency node's {@link DependencyNode#getData() custom data} under which a reference to the
122     * {@link DependencyNode} which has won the conflict is stored.
123     */
124    public static final String NODE_DATA_WINNER = "conflict.winner";
125
126    /**
127     * The key in the dependency node's {@link DependencyNode#getData() custom data} under which the scope of the
128     * dependency before scope derivation and conflict resolution is stored.
129     */
130    public static final String NODE_DATA_ORIGINAL_SCOPE = "conflict.originalScope";
131
132    /**
133     * The key in the dependency node's {@link DependencyNode#getData() custom data} under which the optional flag of
134     * the dependency before derivation and conflict resolution is stored.
135     */
136    public static final String NODE_DATA_ORIGINAL_OPTIONALITY = "conflict.originalOptionality";
137
138    private final VersionSelector versionSelector;
139
140    private final ScopeSelector scopeSelector;
141
142    private final ScopeDeriver scopeDeriver;
143
144    private final OptionalitySelector optionalitySelector;
145
146    /**
147     * Creates a new conflict resolver instance with the specified hooks.
148     *
149     * @param versionSelector The version selector to use, must not be {@code null}.
150     * @param scopeSelector The scope selector to use, must not be {@code null}.
151     * @param optionalitySelector The optionality selector ot use, must not be {@code null}.
152     * @param scopeDeriver The scope deriver to use, must not be {@code null}.
153     */
154    public ConflictResolver(
155            VersionSelector versionSelector,
156            ScopeSelector scopeSelector,
157            OptionalitySelector optionalitySelector,
158            ScopeDeriver scopeDeriver) {
159        this.versionSelector = requireNonNull(versionSelector, "version selector cannot be null");
160        this.scopeSelector = requireNonNull(scopeSelector, "scope selector cannot be null");
161        this.optionalitySelector = requireNonNull(optionalitySelector, "optionality selector cannot be null");
162        this.scopeDeriver = requireNonNull(scopeDeriver, "scope deriver cannot be null");
163    }
164
165    public DependencyNode transformGraph(DependencyNode node, DependencyGraphTransformationContext context)
166            throws RepositoryException {
167        requireNonNull(node, "node cannot be null");
168        requireNonNull(context, "context cannot be null");
169        List<?> sortedConflictIds = (List<?>) context.get(TransformationContextKeys.SORTED_CONFLICT_IDS);
170        if (sortedConflictIds == null) {
171            ConflictIdSorter sorter = new ConflictIdSorter();
172            sorter.transformGraph(node, context);
173
174            sortedConflictIds = (List<?>) context.get(TransformationContextKeys.SORTED_CONFLICT_IDS);
175        }
176
177        @SuppressWarnings("unchecked")
178        Map<String, Object> stats = (Map<String, Object>) context.get(TransformationContextKeys.STATS);
179        long time1 = System.nanoTime();
180
181        @SuppressWarnings("unchecked")
182        Collection<Collection<?>> conflictIdCycles =
183                (Collection<Collection<?>>) context.get(TransformationContextKeys.CYCLIC_CONFLICT_IDS);
184        if (conflictIdCycles == null) {
185            throw new RepositoryException("conflict id cycles have not been identified");
186        }
187
188        Map<?, ?> conflictIds = (Map<?, ?>) context.get(TransformationContextKeys.CONFLICT_IDS);
189        if (conflictIds == null) {
190            throw new RepositoryException("conflict groups have not been identified");
191        }
192
193        Map<Object, Collection<Object>> cyclicPredecessors = new HashMap<>();
194        for (Collection<?> cycle : conflictIdCycles) {
195            for (Object conflictId : cycle) {
196                Collection<Object> predecessors = cyclicPredecessors.computeIfAbsent(conflictId, k -> new HashSet<>());
197                predecessors.addAll(cycle);
198            }
199        }
200
201        State state = new State(node, conflictIds, sortedConflictIds.size(), context);
202        for (Iterator<?> it = sortedConflictIds.iterator(); it.hasNext(); ) {
203            Object conflictId = it.next();
204
205            // reset data structures for next graph walk
206            state.prepare(conflictId, cyclicPredecessors.get(conflictId));
207
208            // find nodes with the current conflict id and while walking the graph (more deeply), nuke leftover losers
209            gatherConflictItems(node, state);
210
211            // now that we know the min depth of the parents, update depth of conflict items
212            state.finish();
213
214            // earlier runs might have nuked all parents of the current conflict id, so it might not exist anymore
215            if (!state.items.isEmpty()) {
216                ConflictContext ctx = state.conflictCtx;
217                state.versionSelector.selectVersion(ctx);
218                if (ctx.winner == null) {
219                    throw new RepositoryException("conflict resolver did not select winner among " + state.items);
220                }
221                DependencyNode winner = ctx.winner.node;
222
223                state.scopeSelector.selectScope(ctx);
224                if (Verbosity.NONE != state.verbosity) {
225                    winner.setData(
226                            NODE_DATA_ORIGINAL_SCOPE, winner.getDependency().getScope());
227                }
228                winner.setScope(ctx.scope);
229
230                state.optionalitySelector.selectOptionality(ctx);
231                if (Verbosity.NONE != state.verbosity) {
232                    winner.setData(
233                            NODE_DATA_ORIGINAL_OPTIONALITY,
234                            winner.getDependency().isOptional());
235                }
236                winner.setOptional(ctx.optional);
237
238                removeLosers(state);
239            }
240
241            // record the winner so we can detect leftover losers during future graph walks
242            state.winner();
243
244            // in case of cycles, trigger final graph walk to ensure all leftover losers are gone
245            if (!it.hasNext() && !conflictIdCycles.isEmpty() && state.conflictCtx.winner != null) {
246                DependencyNode winner = state.conflictCtx.winner.node;
247                state.prepare(state, null);
248                gatherConflictItems(winner, state);
249            }
250        }
251
252        if (stats != null) {
253            long time2 = System.nanoTime();
254            stats.put("ConflictResolver.totalTime", time2 - time1);
255            stats.put("ConflictResolver.conflictItemCount", state.totalConflictItems);
256        }
257
258        return node;
259    }
260
261    private boolean gatherConflictItems(DependencyNode node, State state) throws RepositoryException {
262        Object conflictId = state.conflictIds.get(node);
263        if (state.currentId.equals(conflictId)) {
264            // found it, add conflict item (if not already done earlier by another path)
265            state.add(node);
266            // we don't recurse here so we might miss losers beneath us, those will be nuked during future walks below
267        } else if (state.loser(node, conflictId)) {
268            // found a leftover loser (likely in a cycle) of an already processed conflict id, tell caller to nuke it
269            return false;
270        } else if (state.push(node, conflictId)) {
271            // found potential parent, no cycle and not visisted before with the same derived scope, so recurse
272            for (Iterator<DependencyNode> it = node.getChildren().iterator(); it.hasNext(); ) {
273                DependencyNode child = it.next();
274                if (!gatherConflictItems(child, state)) {
275                    it.remove();
276                }
277            }
278            state.pop();
279        }
280        return true;
281    }
282
283    private static void removeLosers(State state) {
284        ConflictItem winner = state.conflictCtx.winner;
285        String winnerArtifactId = ArtifactIdUtils.toId(winner.node.getArtifact());
286        List<DependencyNode> previousParent = null;
287        ListIterator<DependencyNode> childIt = null;
288        HashSet<String> toRemoveIds = new HashSet<>();
289        for (ConflictItem item : state.items) {
290            if (item == winner) {
291                continue;
292            }
293            if (item.parent != previousParent) {
294                childIt = item.parent.listIterator();
295                previousParent = item.parent;
296            }
297            while (childIt.hasNext()) {
298                DependencyNode child = childIt.next();
299                if (child == item.node) {
300                    // NONE: just remove it and done
301                    if (Verbosity.NONE == state.verbosity) {
302                        childIt.remove();
303                        break;
304                    }
305
306                    // STANDARD: doing extra bookkeeping to select "which nodes to remove"
307                    if (Verbosity.STANDARD == state.verbosity) {
308                        String childArtifactId = ArtifactIdUtils.toId(child.getArtifact());
309                        // if two IDs are equal, it means "there is nearest", not conflict per se.
310                        // In that case we do NOT allow this child to be removed (but remove others)
311                        // and this keeps us safe from iteration (and in general, version) ordering
312                        // as we explicitly leave out ID that is "nearest found" state.
313                        //
314                        // This tackles version ranges mostly, where ranges are turned into list of
315                        // several nodes in collector (as many were discovered, ie. from metadata), and
316                        // old code would just "mark" the first hit as conflict, and remove the rest,
317                        // even if rest could contain "more suitable" version, that is not conflicting/diverging.
318                        // This resulted in verbose mode transformed tree, that was misrepresenting things
319                        // for dependency convergence calculations: it represented state like parent node
320                        // depends on "wrong" version (diverge), while "right" version was present (but removed)
321                        // as well, as it was contained in parents version range.
322                        if (!Objects.equals(winnerArtifactId, childArtifactId)) {
323                            toRemoveIds.add(childArtifactId);
324                        }
325                    }
326
327                    // FULL: just record the facts
328                    DependencyNode loser = new DefaultDependencyNode(child);
329                    loser.setData(NODE_DATA_WINNER, winner.node);
330                    loser.setData(
331                            NODE_DATA_ORIGINAL_SCOPE, loser.getDependency().getScope());
332                    loser.setData(
333                            NODE_DATA_ORIGINAL_OPTIONALITY,
334                            loser.getDependency().isOptional());
335                    loser.setScope(item.getScopes().iterator().next());
336                    loser.setChildren(Collections.emptyList());
337                    childIt.set(loser);
338                    item.node = loser;
339                    break;
340                }
341            }
342        }
343
344        // 2nd pass to apply "standard" verbosity: leaving only 1 loser, but with care
345        if (Verbosity.STANDARD == state.verbosity && !toRemoveIds.isEmpty()) {
346            previousParent = null;
347            for (ConflictItem item : state.items) {
348                if (item == winner) {
349                    continue;
350                }
351                if (item.parent != previousParent) {
352                    childIt = item.parent.listIterator();
353                    previousParent = item.parent;
354                }
355                while (childIt.hasNext()) {
356                    DependencyNode child = childIt.next();
357                    if (child == item.node) {
358                        String childArtifactId = ArtifactIdUtils.toId(child.getArtifact());
359                        if (toRemoveIds.contains(childArtifactId)
360                                && relatedSiblingsCount(child.getArtifact(), item.parent) > 1) {
361                            childIt.remove();
362                        }
363                        break;
364                    }
365                }
366            }
367        }
368
369        // there might still be losers beneath the winner (e.g. in case of cycles)
370        // those will be nuked during future graph walks when we include the winner in the recursion
371    }
372
373    private static long relatedSiblingsCount(Artifact artifact, List<DependencyNode> parent) {
374        String ga = artifact.getGroupId() + ":" + artifact.getArtifactId();
375        return parent.stream()
376                .map(DependencyNode::getArtifact)
377                .filter(a -> ga.equals(a.getGroupId() + ":" + a.getArtifactId()))
378                .count();
379    }
380
381    static final class NodeInfo {
382
383        /**
384         * The smallest depth at which the node was seen, used for "the" depth of its conflict items.
385         */
386        int minDepth;
387
388        /**
389         * The set of derived scopes the node was visited with, used to check whether an already seen node needs to be
390         * revisited again in context of another scope. To conserve memory, we start with {@code String} and update to
391         * {@code Set<String>} if needed.
392         */
393        Object derivedScopes;
394
395        /**
396         * The set of derived optionalities the node was visited with, used to check whether an already seen node needs
397         * to be revisited again in context of another optionality. To conserve memory, encoded as bit field (bit 0 ->
398         * optional=false, bit 1 -> optional=true).
399         */
400        int derivedOptionalities;
401
402        /**
403         * The conflict items which are immediate children of the node, used to easily update those conflict items after
404         * a new parent scope/optionality was encountered.
405         */
406        List<ConflictItem> children;
407
408        static final int CHANGE_SCOPE = 0x01;
409
410        static final int CHANGE_OPTIONAL = 0x02;
411
412        private static final int OPT_FALSE = 0x01;
413
414        private static final int OPT_TRUE = 0x02;
415
416        NodeInfo(int depth, String derivedScope, boolean optional) {
417            minDepth = depth;
418            derivedScopes = derivedScope;
419            derivedOptionalities = optional ? OPT_TRUE : OPT_FALSE;
420        }
421
422        @SuppressWarnings("unchecked")
423        int update(int depth, String derivedScope, boolean optional) {
424            if (depth < minDepth) {
425                minDepth = depth;
426            }
427            int changes;
428            if (derivedScopes.equals(derivedScope)) {
429                changes = 0;
430            } else if (derivedScopes instanceof Collection) {
431                changes = ((Collection<String>) derivedScopes).add(derivedScope) ? CHANGE_SCOPE : 0;
432            } else {
433                Collection<String> scopes = new HashSet<>();
434                scopes.add((String) derivedScopes);
435                scopes.add(derivedScope);
436                derivedScopes = scopes;
437                changes = CHANGE_SCOPE;
438            }
439            int bit = optional ? OPT_TRUE : OPT_FALSE;
440            if ((derivedOptionalities & bit) == 0) {
441                derivedOptionalities |= bit;
442                changes |= CHANGE_OPTIONAL;
443            }
444            return changes;
445        }
446
447        void add(ConflictItem item) {
448            if (children == null) {
449                children = new ArrayList<>(1);
450            }
451            children.add(item);
452        }
453    }
454
455    final class State {
456
457        /**
458         * The conflict id currently processed.
459         */
460        Object currentId;
461
462        /**
463         * Stats counter.
464         */
465        int totalConflictItems;
466
467        /**
468         * Flag whether we should keep losers in the graph to enable visualization/troubleshooting of conflicts.
469         */
470        final Verbosity verbosity;
471
472        /**
473         * A mapping from conflict id to winner node, helps to recognize nodes that have their effective
474         * scope&optionality set or are leftovers from previous removals.
475         */
476        final Map<Object, DependencyNode> resolvedIds;
477
478        /**
479         * The set of conflict ids which could apply to ancestors of nodes with the current conflict id, used to avoid
480         * recursion early on. This is basically a superset of the key set of resolvedIds, the additional ids account
481         * for cyclic dependencies.
482         */
483        final Collection<Object> potentialAncestorIds;
484
485        /**
486         * The output from the conflict marker
487         */
488        final Map<?, ?> conflictIds;
489
490        /**
491         * The conflict items we have gathered so far for the current conflict id.
492         */
493        final List<ConflictItem> items;
494
495        /**
496         * The (conceptual) mapping from nodes to extra infos, technically keyed by the node's child list which better
497         * captures the identity of a node since we're basically concerned with effects towards children.
498         */
499        final Map<List<DependencyNode>, NodeInfo> infos;
500
501        /**
502         * The set of nodes on the DFS stack to detect cycles, technically keyed by the node's child list to match the
503         * dirty graph structure produced by the dependency collector for cycles.
504         */
505        final Map<List<DependencyNode>, Object> stack;
506
507        /**
508         * The stack of parent nodes.
509         */
510        final List<DependencyNode> parentNodes;
511
512        /**
513         * The stack of derived scopes for parent nodes.
514         */
515        final List<String> parentScopes;
516
517        /**
518         * The stack of derived optional flags for parent nodes.
519         */
520        final List<Boolean> parentOptionals;
521
522        /**
523         * The stack of node infos for parent nodes, may contain {@code null} which is used to disable creating new
524         * conflict items when visiting their parent again (conflict items are meant to be unique by parent-node combo).
525         */
526        final List<NodeInfo> parentInfos;
527
528        /**
529         * The conflict context passed to the version/scope/optionality selectors, updated as we move along rather than
530         * recreated to avoid tmp objects.
531         */
532        final ConflictContext conflictCtx;
533
534        /**
535         * The scope context passed to the scope deriver, updated as we move along rather than recreated to avoid tmp
536         * objects.
537         */
538        final ScopeContext scopeCtx;
539
540        /**
541         * The effective version selector, i.e. after initialization.
542         */
543        final VersionSelector versionSelector;
544
545        /**
546         * The effective scope selector, i.e. after initialization.
547         */
548        final ScopeSelector scopeSelector;
549
550        /**
551         * The effective scope deriver, i.e. after initialization.
552         */
553        final ScopeDeriver scopeDeriver;
554
555        /**
556         * The effective optionality selector, i.e. after initialization.
557         */
558        final OptionalitySelector optionalitySelector;
559
560        State(
561                DependencyNode root,
562                Map<?, ?> conflictIds,
563                int conflictIdCount,
564                DependencyGraphTransformationContext context)
565                throws RepositoryException {
566            this.conflictIds = conflictIds;
567            this.verbosity = getVerbosity(context.getSession());
568            potentialAncestorIds = new HashSet<>(conflictIdCount * 2);
569            resolvedIds = new HashMap<>(conflictIdCount * 2);
570            items = new ArrayList<>(256);
571            infos = new IdentityHashMap<>(64);
572            stack = new IdentityHashMap<>(64);
573            parentNodes = new ArrayList<>(64);
574            parentScopes = new ArrayList<>(64);
575            parentOptionals = new ArrayList<>(64);
576            parentInfos = new ArrayList<>(64);
577            conflictCtx = new ConflictContext(root, conflictIds, items);
578            scopeCtx = new ScopeContext(null, null);
579            versionSelector = ConflictResolver.this.versionSelector.getInstance(root, context);
580            scopeSelector = ConflictResolver.this.scopeSelector.getInstance(root, context);
581            scopeDeriver = ConflictResolver.this.scopeDeriver.getInstance(root, context);
582            optionalitySelector = ConflictResolver.this.optionalitySelector.getInstance(root, context);
583        }
584
585        void prepare(Object conflictId, Collection<Object> cyclicPredecessors) {
586            currentId = conflictId;
587            conflictCtx.conflictId = conflictId;
588            conflictCtx.winner = null;
589            conflictCtx.scope = null;
590            conflictCtx.optional = null;
591            items.clear();
592            infos.clear();
593            if (cyclicPredecessors != null) {
594                potentialAncestorIds.addAll(cyclicPredecessors);
595            }
596        }
597
598        void finish() {
599            List<DependencyNode> previousParent = null;
600            int previousDepth = 0;
601            totalConflictItems += items.size();
602            for (ListIterator<ConflictItem> iterator = items.listIterator(items.size()); iterator.hasPrevious(); ) {
603                ConflictItem item = iterator.previous();
604                if (item.parent == previousParent) {
605                    item.depth = previousDepth;
606                } else if (item.parent != null) {
607                    previousParent = item.parent;
608                    NodeInfo info = infos.get(previousParent);
609                    previousDepth = info.minDepth + 1;
610                    item.depth = previousDepth;
611                }
612            }
613            potentialAncestorIds.add(currentId);
614        }
615
616        void winner() {
617            resolvedIds.put(currentId, (conflictCtx.winner != null) ? conflictCtx.winner.node : null);
618        }
619
620        boolean loser(DependencyNode node, Object conflictId) {
621            DependencyNode winner = resolvedIds.get(conflictId);
622            return winner != null && winner != node;
623        }
624
625        boolean push(DependencyNode node, Object conflictId) throws RepositoryException {
626            if (conflictId == null) {
627                if (node.getDependency() != null) {
628                    if (node.getData().get(NODE_DATA_WINNER) != null) {
629                        return false;
630                    }
631                    throw new RepositoryException("missing conflict id for node " + node);
632                }
633            } else if (!potentialAncestorIds.contains(conflictId)) {
634                return false;
635            }
636
637            List<DependencyNode> graphNode = node.getChildren();
638            if (stack.put(graphNode, Boolean.TRUE) != null) {
639                return false;
640            }
641
642            int depth = depth();
643            String scope = deriveScope(node, conflictId);
644            boolean optional = deriveOptional(node, conflictId);
645            NodeInfo info = infos.get(graphNode);
646            if (info == null) {
647                info = new NodeInfo(depth, scope, optional);
648                infos.put(graphNode, info);
649                parentInfos.add(info);
650                parentNodes.add(node);
651                parentScopes.add(scope);
652                parentOptionals.add(optional);
653            } else {
654                int changes = info.update(depth, scope, optional);
655                if (changes == 0) {
656                    stack.remove(graphNode);
657                    return false;
658                }
659                parentInfos.add(null); // disable creating new conflict items, we update the existing ones below
660                parentNodes.add(node);
661                parentScopes.add(scope);
662                parentOptionals.add(optional);
663                if (info.children != null) {
664                    if ((changes & NodeInfo.CHANGE_SCOPE) != 0) {
665                        ListIterator<ConflictItem> itemIterator = info.children.listIterator(info.children.size());
666                        while (itemIterator.hasPrevious()) {
667                            ConflictItem item = itemIterator.previous();
668                            String childScope = deriveScope(item.node, null);
669                            item.addScope(childScope);
670                        }
671                    }
672                    if ((changes & NodeInfo.CHANGE_OPTIONAL) != 0) {
673                        ListIterator<ConflictItem> itemIterator = info.children.listIterator(info.children.size());
674                        while (itemIterator.hasPrevious()) {
675                            ConflictItem item = itemIterator.previous();
676                            boolean childOptional = deriveOptional(item.node, null);
677                            item.addOptional(childOptional);
678                        }
679                    }
680                }
681            }
682
683            return true;
684        }
685
686        void pop() {
687            int last = parentInfos.size() - 1;
688            parentInfos.remove(last);
689            parentScopes.remove(last);
690            parentOptionals.remove(last);
691            DependencyNode node = parentNodes.remove(last);
692            stack.remove(node.getChildren());
693        }
694
695        void add(DependencyNode node) throws RepositoryException {
696            DependencyNode parent = parent();
697            if (parent == null) {
698                ConflictItem item = newConflictItem(parent, node);
699                items.add(item);
700            } else {
701                NodeInfo info = parentInfos.get(parentInfos.size() - 1);
702                if (info != null) {
703                    ConflictItem item = newConflictItem(parent, node);
704                    info.add(item);
705                    items.add(item);
706                }
707            }
708        }
709
710        private ConflictItem newConflictItem(DependencyNode parent, DependencyNode node) throws RepositoryException {
711            return new ConflictItem(parent, node, deriveScope(node, null), deriveOptional(node, null));
712        }
713
714        private int depth() {
715            return parentNodes.size();
716        }
717
718        private DependencyNode parent() {
719            int size = parentNodes.size();
720            return (size <= 0) ? null : parentNodes.get(size - 1);
721        }
722
723        private String deriveScope(DependencyNode node, Object conflictId) throws RepositoryException {
724            if ((node.getManagedBits() & DependencyNode.MANAGED_SCOPE) != 0
725                    || (conflictId != null && resolvedIds.containsKey(conflictId))) {
726                return scope(node.getDependency());
727            }
728
729            int depth = parentNodes.size();
730            scopes(depth, node.getDependency());
731            if (depth > 0) {
732                scopeDeriver.deriveScope(scopeCtx);
733            }
734            return scopeCtx.derivedScope;
735        }
736
737        private void scopes(int parent, Dependency child) {
738            scopeCtx.parentScope = (parent > 0) ? parentScopes.get(parent - 1) : null;
739            scopeCtx.derivedScope = scope(child);
740            scopeCtx.childScope = scope(child);
741        }
742
743        private String scope(Dependency dependency) {
744            return (dependency != null) ? dependency.getScope() : null;
745        }
746
747        private boolean deriveOptional(DependencyNode node, Object conflictId) {
748            Dependency dep = node.getDependency();
749            boolean optional = (dep != null) && dep.isOptional();
750            if (optional
751                    || (node.getManagedBits() & DependencyNode.MANAGED_OPTIONAL) != 0
752                    || (conflictId != null && resolvedIds.containsKey(conflictId))) {
753                return optional;
754            }
755            int depth = parentNodes.size();
756            return (depth > 0) ? parentOptionals.get(depth - 1) : false;
757        }
758    }
759
760    /**
761     * A context used to hold information that is relevant for deriving the scope of a child dependency.
762     *
763     * @see ScopeDeriver
764     * @noinstantiate This class is not intended to be instantiated by clients in production code, the constructor may
765     *                change without notice and only exists to enable unit testing.
766     */
767    public static final class ScopeContext {
768
769        String parentScope;
770
771        String childScope;
772
773        String derivedScope;
774
775        /**
776         * Creates a new scope context with the specified properties.
777         *
778         * @param parentScope The scope of the parent dependency, may be {@code null}.
779         * @param childScope The scope of the child dependency, may be {@code null}.
780         * @noreference This class is not intended to be instantiated by clients in production code, the constructor may
781         *              change without notice and only exists to enable unit testing.
782         */
783        public ScopeContext(String parentScope, String childScope) {
784            this.parentScope = (parentScope != null) ? parentScope : "";
785            derivedScope = (childScope != null) ? childScope : "";
786            this.childScope = (childScope != null) ? childScope : "";
787        }
788
789        /**
790         * Gets the scope of the parent dependency. This is usually the scope that was derived by earlier invocations of
791         * the scope deriver.
792         *
793         * @return The scope of the parent dependency, never {@code null}.
794         */
795        public String getParentScope() {
796            return parentScope;
797        }
798
799        /**
800         * Gets the original scope of the child dependency. This is the scope that was declared in the artifact
801         * descriptor of the parent dependency.
802         *
803         * @return The original scope of the child dependency, never {@code null}.
804         */
805        public String getChildScope() {
806            return childScope;
807        }
808
809        /**
810         * Gets the derived scope of the child dependency. This is initially equal to {@link #getChildScope()} until the
811         * scope deriver makes changes.
812         *
813         * @return The derived scope of the child dependency, never {@code null}.
814         */
815        public String getDerivedScope() {
816            return derivedScope;
817        }
818
819        /**
820         * Sets the derived scope of the child dependency.
821         *
822         * @param derivedScope The derived scope of the dependency, may be {@code null}.
823         */
824        public void setDerivedScope(String derivedScope) {
825            this.derivedScope = (derivedScope != null) ? derivedScope : "";
826        }
827    }
828
829    /**
830     * A conflicting dependency.
831     *
832     * @noinstantiate This class is not intended to be instantiated by clients in production code, the constructor may
833     *                change without notice and only exists to enable unit testing.
834     */
835    public static final class ConflictItem {
836
837        // nodes can share child lists, we care about the unique owner of a child node which is the child list
838        final List<DependencyNode> parent;
839
840        // only for debugging/toString() to help identify the parent node(s)
841        final Artifact artifact;
842
843        // is mutable as removeLosers will mutate it (if Verbosity==STANDARD)
844        DependencyNode node;
845
846        int depth;
847
848        // we start with String and update to Set<String> if needed
849        Object scopes;
850
851        // bit field of OPTIONAL_FALSE and OPTIONAL_TRUE
852        int optionalities;
853
854        /**
855         * Bit flag indicating whether one or more paths consider the dependency non-optional.
856         */
857        public static final int OPTIONAL_FALSE = 0x01;
858
859        /**
860         * Bit flag indicating whether one or more paths consider the dependency optional.
861         */
862        public static final int OPTIONAL_TRUE = 0x02;
863
864        ConflictItem(DependencyNode parent, DependencyNode node, String scope, boolean optional) {
865            if (parent != null) {
866                this.parent = parent.getChildren();
867                this.artifact = parent.getArtifact();
868            } else {
869                this.parent = null;
870                this.artifact = null;
871            }
872            this.node = node;
873            this.scopes = scope;
874            this.optionalities = optional ? OPTIONAL_TRUE : OPTIONAL_FALSE;
875        }
876
877        /**
878         * Creates a new conflict item with the specified properties.
879         *
880         * @param parent The parent node of the conflicting dependency, may be {@code null}.
881         * @param node The conflicting dependency, must not be {@code null}.
882         * @param depth The zero-based depth of the conflicting dependency.
883         * @param optionalities The optionalities the dependency was encountered with, encoded as a bit field consisting
884         *            of {@link ConflictResolver.ConflictItem#OPTIONAL_TRUE} and
885         *            {@link ConflictResolver.ConflictItem#OPTIONAL_FALSE}.
886         * @param scopes The derived scopes of the conflicting dependency, must not be {@code null}.
887         * @noreference This class is not intended to be instantiated by clients in production code, the constructor may
888         *              change without notice and only exists to enable unit testing.
889         */
890        public ConflictItem(
891                DependencyNode parent, DependencyNode node, int depth, int optionalities, String... scopes) {
892            this.parent = (parent != null) ? parent.getChildren() : null;
893            this.artifact = (parent != null) ? parent.getArtifact() : null;
894            this.node = node;
895            this.depth = depth;
896            this.optionalities = optionalities;
897            this.scopes = Arrays.asList(scopes);
898        }
899
900        /**
901         * Determines whether the specified conflict item is a sibling of this item.
902         *
903         * @param item The other conflict item, must not be {@code null}.
904         * @return {@code true} if the given item has the same parent as this item, {@code false} otherwise.
905         */
906        public boolean isSibling(ConflictItem item) {
907            return parent == item.parent;
908        }
909
910        /**
911         * Gets the dependency node involved in the conflict.
912         *
913         * @return The involved dependency node, never {@code null}.
914         */
915        public DependencyNode getNode() {
916            return node;
917        }
918
919        /**
920         * Gets the dependency involved in the conflict, short for {@code getNode.getDependency()}.
921         *
922         * @return The involved dependency, never {@code null}.
923         */
924        public Dependency getDependency() {
925            return node.getDependency();
926        }
927
928        /**
929         * Gets the zero-based depth at which the conflicting node occurs in the graph. As such, the depth denotes the
930         * number of parent nodes. If actually multiple paths lead to the node, the return value denotes the smallest
931         * possible depth.
932         *
933         * @return The zero-based depth of the node in the graph.
934         */
935        public int getDepth() {
936            return depth;
937        }
938
939        /**
940         * Gets the derived scopes of the dependency. In general, the same dependency node could be reached via
941         * different paths and each path might result in a different derived scope.
942         *
943         * @see ScopeDeriver
944         * @return The (read-only) set of derived scopes of the dependency, never {@code null}.
945         */
946        @SuppressWarnings("unchecked")
947        public Collection<String> getScopes() {
948            if (scopes instanceof String) {
949                return Collections.singleton((String) scopes);
950            }
951            return (Collection<String>) scopes;
952        }
953
954        @SuppressWarnings("unchecked")
955        void addScope(String scope) {
956            if (scopes instanceof Collection) {
957                ((Collection<String>) scopes).add(scope);
958            } else if (!scopes.equals(scope)) {
959                Collection<Object> set = new HashSet<>();
960                set.add(scopes);
961                set.add(scope);
962                scopes = set;
963            }
964        }
965
966        /**
967         * Gets the derived optionalities of the dependency. In general, the same dependency node could be reached via
968         * different paths and each path might result in a different derived optionality.
969         *
970         * @return A bit field consisting of {@link ConflictResolver.ConflictItem#OPTIONAL_FALSE} and/or
971         *         {@link ConflictResolver.ConflictItem#OPTIONAL_TRUE} indicating the derived optionalities the
972         *         dependency was encountered with.
973         */
974        public int getOptionalities() {
975            return optionalities;
976        }
977
978        void addOptional(boolean optional) {
979            optionalities |= optional ? OPTIONAL_TRUE : OPTIONAL_FALSE;
980        }
981
982        @Override
983        public String toString() {
984            return node + " @ " + depth + " < " + artifact;
985        }
986    }
987
988    /**
989     * A context used to hold information that is relevant for resolving version and scope conflicts.
990     *
991     * @see VersionSelector
992     * @see ScopeSelector
993     * @noinstantiate This class is not intended to be instantiated by clients in production code, the constructor may
994     *                change without notice and only exists to enable unit testing.
995     */
996    public static final class ConflictContext {
997
998        final DependencyNode root;
999
1000        final Map<?, ?> conflictIds;
1001
1002        final Collection<ConflictItem> items;
1003
1004        Object conflictId;
1005
1006        ConflictItem winner;
1007
1008        String scope;
1009
1010        Boolean optional;
1011
1012        ConflictContext(DependencyNode root, Map<?, ?> conflictIds, Collection<ConflictItem> items) {
1013            this.root = root;
1014            this.conflictIds = conflictIds;
1015            this.items = Collections.unmodifiableCollection(items);
1016        }
1017
1018        /**
1019         * Creates a new conflict context.
1020         *
1021         * @param root The root node of the dependency graph, must not be {@code null}.
1022         * @param conflictId The conflict id for the set of conflicting dependencies in this context, must not be
1023         *            {@code null}.
1024         * @param conflictIds The mapping from dependency node to conflict id, must not be {@code null}.
1025         * @param items The conflict items in this context, must not be {@code null}.
1026         * @noreference This class is not intended to be instantiated by clients in production code, the constructor may
1027         *              change without notice and only exists to enable unit testing.
1028         */
1029        public ConflictContext(
1030                DependencyNode root,
1031                Object conflictId,
1032                Map<DependencyNode, Object> conflictIds,
1033                Collection<ConflictItem> items) {
1034            this(root, conflictIds, items);
1035            this.conflictId = conflictId;
1036        }
1037
1038        /**
1039         * Gets the root node of the dependency graph being transformed.
1040         *
1041         * @return The root node of the dependeny graph, never {@code null}.
1042         */
1043        public DependencyNode getRoot() {
1044            return root;
1045        }
1046
1047        /**
1048         * Determines whether the specified dependency node belongs to this conflict context.
1049         *
1050         * @param node The dependency node to check, must not be {@code null}.
1051         * @return {@code true} if the given node belongs to this conflict context, {@code false} otherwise.
1052         */
1053        public boolean isIncluded(DependencyNode node) {
1054            return conflictId.equals(conflictIds.get(node));
1055        }
1056
1057        /**
1058         * Gets the collection of conflict items in this context.
1059         *
1060         * @return The (read-only) collection of conflict items in this context, never {@code null}.
1061         */
1062        public Collection<ConflictItem> getItems() {
1063            return items;
1064        }
1065
1066        /**
1067         * Gets the conflict item which has been selected as the winner among the conflicting dependencies.
1068         *
1069         * @return The winning conflict item or {@code null} if not set yet.
1070         */
1071        public ConflictItem getWinner() {
1072            return winner;
1073        }
1074
1075        /**
1076         * Sets the conflict item which has been selected as the winner among the conflicting dependencies.
1077         *
1078         * @param winner The winning conflict item, may be {@code null}.
1079         */
1080        public void setWinner(ConflictItem winner) {
1081            this.winner = winner;
1082        }
1083
1084        /**
1085         * Gets the effective scope of the winning dependency.
1086         *
1087         * @return The effective scope of the winning dependency or {@code null} if none.
1088         */
1089        public String getScope() {
1090            return scope;
1091        }
1092
1093        /**
1094         * Sets the effective scope of the winning dependency.
1095         *
1096         * @param scope The effective scope, may be {@code null}.
1097         */
1098        public void setScope(String scope) {
1099            this.scope = scope;
1100        }
1101
1102        /**
1103         * Gets the effective optional flag of the winning dependency.
1104         *
1105         * @return The effective optional flag or {@code null} if none.
1106         */
1107        public Boolean getOptional() {
1108            return optional;
1109        }
1110
1111        /**
1112         * Sets the effective optional flag of the winning dependency.
1113         *
1114         * @param optional The effective optional flag, may be {@code null}.
1115         */
1116        public void setOptional(Boolean optional) {
1117            this.optional = optional;
1118        }
1119
1120        @Override
1121        public String toString() {
1122            return winner + " @ " + scope + " < " + items;
1123        }
1124    }
1125
1126    /**
1127     * An extension point of {@link ConflictResolver} that determines the winner among conflicting dependencies. The
1128     * winning node (and its children) will be retained in the dependency graph, the other nodes will get removed. The
1129     * version selector does not need to deal with potential scope conflicts, these will be addressed afterwards by the
1130     * {@link ScopeSelector}.
1131     * <p>
1132     * <strong>Note:</strong> Implementations must be stateless.
1133     */
1134    public abstract static class VersionSelector {
1135
1136        /**
1137         * Retrieves the version selector for use during the specified graph transformation. The conflict resolver calls
1138         * this method once per
1139         * {@link ConflictResolver#transformGraph(DependencyNode, DependencyGraphTransformationContext)} invocation to
1140         * allow implementations to prepare any auxiliary data that is needed for their operation. Given that
1141         * implementations must be stateless, a new instance needs to be returned to hold such auxiliary data. The
1142         * default implementation simply returns the current instance which is appropriate for implementations which do
1143         * not require auxiliary data.
1144         *
1145         * @param root The root node of the (possibly cyclic!) graph to transform, must not be {@code null}.
1146         * @param context The graph transformation context, must not be {@code null}.
1147         * @return The scope deriver to use for the given graph transformation, never {@code null}.
1148         * @throws RepositoryException If the instance could not be retrieved.
1149         */
1150        public VersionSelector getInstance(DependencyNode root, DependencyGraphTransformationContext context)
1151                throws RepositoryException {
1152            return this;
1153        }
1154
1155        /**
1156         * Determines the winning node among conflicting dependencies. Implementations will usually iterate
1157         * {@link ConflictContext#getItems()}, inspect {@link ConflictItem#getNode()} and eventually call
1158         * {@link ConflictContext#setWinner(ConflictResolver.ConflictItem)} to deliver the winner. Failure to select a
1159         * winner will automatically fail the entire conflict resolution.
1160         *
1161         * @param context The conflict context, must not be {@code null}.
1162         * @throws RepositoryException If the version selection failed.
1163         */
1164        public abstract void selectVersion(ConflictContext context) throws RepositoryException;
1165    }
1166
1167    /**
1168     * An extension point of {@link ConflictResolver} that determines the effective scope of a dependency from a
1169     * potentially conflicting set of {@link ScopeDeriver derived scopes}. The scope selector gets invoked after the
1170     * {@link VersionSelector} has picked the winning node.
1171     * <p>
1172     * <strong>Note:</strong> Implementations must be stateless.
1173     */
1174    public abstract static class ScopeSelector {
1175
1176        /**
1177         * Retrieves the scope selector for use during the specified graph transformation. The conflict resolver calls
1178         * this method once per
1179         * {@link ConflictResolver#transformGraph(DependencyNode, DependencyGraphTransformationContext)} invocation to
1180         * allow implementations to prepare any auxiliary data that is needed for their operation. Given that
1181         * implementations must be stateless, a new instance needs to be returned to hold such auxiliary data. The
1182         * default implementation simply returns the current instance which is appropriate for implementations which do
1183         * not require auxiliary data.
1184         *
1185         * @param root The root node of the (possibly cyclic!) graph to transform, must not be {@code null}.
1186         * @param context The graph transformation context, must not be {@code null}.
1187         * @return The scope selector to use for the given graph transformation, never {@code null}.
1188         * @throws RepositoryException If the instance could not be retrieved.
1189         */
1190        public ScopeSelector getInstance(DependencyNode root, DependencyGraphTransformationContext context)
1191                throws RepositoryException {
1192            return this;
1193        }
1194
1195        /**
1196         * Determines the effective scope of the dependency given by {@link ConflictContext#getWinner()}.
1197         * Implementations will usually iterate {@link ConflictContext#getItems()}, inspect
1198         * {@link ConflictItem#getScopes()} and eventually call {@link ConflictContext#setScope(String)} to deliver the
1199         * effective scope.
1200         *
1201         * @param context The conflict context, must not be {@code null}.
1202         * @throws RepositoryException If the scope selection failed.
1203         */
1204        public abstract void selectScope(ConflictContext context) throws RepositoryException;
1205    }
1206
1207    /**
1208     * An extension point of {@link ConflictResolver} that determines the scope of a dependency in relation to the scope
1209     * of its parent.
1210     * <p>
1211     * <strong>Note:</strong> Implementations must be stateless.
1212     */
1213    public abstract static class ScopeDeriver {
1214
1215        /**
1216         * Retrieves the scope deriver for use during the specified graph transformation. The conflict resolver calls
1217         * this method once per
1218         * {@link ConflictResolver#transformGraph(DependencyNode, DependencyGraphTransformationContext)} invocation to
1219         * allow implementations to prepare any auxiliary data that is needed for their operation. Given that
1220         * implementations must be stateless, a new instance needs to be returned to hold such auxiliary data. The
1221         * default implementation simply returns the current instance which is appropriate for implementations which do
1222         * not require auxiliary data.
1223         *
1224         * @param root The root node of the (possibly cyclic!) graph to transform, must not be {@code null}.
1225         * @param context The graph transformation context, must not be {@code null}.
1226         * @return The scope deriver to use for the given graph transformation, never {@code null}.
1227         * @throws RepositoryException If the instance could not be retrieved.
1228         */
1229        public ScopeDeriver getInstance(DependencyNode root, DependencyGraphTransformationContext context)
1230                throws RepositoryException {
1231            return this;
1232        }
1233
1234        /**
1235         * Determines the scope of a dependency in relation to the scope of its parent. Implementors need to call
1236         * {@link ScopeContext#setDerivedScope(String)} to deliver the result of their calculation. If said method is
1237         * not invoked, the conflict resolver will assume the scope of the child dependency remains unchanged.
1238         *
1239         * @param context The scope context, must not be {@code null}.
1240         * @throws RepositoryException If the scope deriviation failed.
1241         */
1242        public abstract void deriveScope(ScopeContext context) throws RepositoryException;
1243    }
1244
1245    /**
1246     * An extension point of {@link ConflictResolver} that determines the effective optional flag of a dependency from a
1247     * potentially conflicting set of derived optionalities. The optionality selector gets invoked after the
1248     * {@link VersionSelector} has picked the winning node.
1249     * <p>
1250     * <strong>Note:</strong> Implementations must be stateless.
1251     */
1252    public abstract static class OptionalitySelector {
1253
1254        /**
1255         * Retrieves the optionality selector for use during the specified graph transformation. The conflict resolver
1256         * calls this method once per
1257         * {@link ConflictResolver#transformGraph(DependencyNode, DependencyGraphTransformationContext)} invocation to
1258         * allow implementations to prepare any auxiliary data that is needed for their operation. Given that
1259         * implementations must be stateless, a new instance needs to be returned to hold such auxiliary data. The
1260         * default implementation simply returns the current instance which is appropriate for implementations which do
1261         * not require auxiliary data.
1262         *
1263         * @param root The root node of the (possibly cyclic!) graph to transform, must not be {@code null}.
1264         * @param context The graph transformation context, must not be {@code null}.
1265         * @return The optionality selector to use for the given graph transformation, never {@code null}.
1266         * @throws RepositoryException If the instance could not be retrieved.
1267         */
1268        public OptionalitySelector getInstance(DependencyNode root, DependencyGraphTransformationContext context)
1269                throws RepositoryException {
1270            return this;
1271        }
1272
1273        /**
1274         * Determines the effective optional flag of the dependency given by {@link ConflictContext#getWinner()}.
1275         * Implementations will usually iterate {@link ConflictContext#getItems()}, inspect
1276         * {@link ConflictItem#getOptionalities()} and eventually call {@link ConflictContext#setOptional(Boolean)} to
1277         * deliver the effective optional flag.
1278         *
1279         * @param context The conflict context, must not be {@code null}.
1280         * @throws RepositoryException If the optionality selection failed.
1281         */
1282        public abstract void selectOptionality(ConflictContext context) throws RepositoryException;
1283    }
1284}