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.graph;
020
021import java.util.AbstractSet;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.Iterator;
025import java.util.LinkedHashSet;
026import java.util.NoSuchElementException;
027import java.util.Objects;
028import java.util.Set;
029
030import org.eclipse.aether.artifact.Artifact;
031
032import static java.util.Objects.requireNonNull;
033
034/**
035 * A dependency to some artifact. <em>Note:</em> Instances of this class are immutable and the exposed mutators return
036 * new objects rather than changing the current instance.
037 */
038public final class Dependency {
039
040    private final Artifact artifact;
041
042    private final String scope;
043
044    private final Boolean optional;
045
046    private final Set<Exclusion> exclusions;
047
048    /**
049     * Creates a mandatory dependency on the specified artifact with the given scope.
050     *
051     * @param artifact The artifact being depended on, must not be {@code null}.
052     * @param scope The scope of the dependency, may be {@code null}.
053     */
054    public Dependency(Artifact artifact, String scope) {
055        this(artifact, scope, false);
056    }
057
058    /**
059     * Creates a dependency on the specified artifact with the given scope.
060     *
061     * @param artifact The artifact being depended on, must not be {@code null}.
062     * @param scope The scope of the dependency, may be {@code null}.
063     * @param optional A flag whether the dependency is optional or mandatory, may be {@code null}.
064     */
065    public Dependency(Artifact artifact, String scope, Boolean optional) {
066        this(artifact, scope, optional, null);
067    }
068
069    /**
070     * Creates a dependency on the specified artifact with the given scope and exclusions.
071     *
072     * @param artifact The artifact being depended on, must not be {@code null}.
073     * @param scope The scope of the dependency, may be {@code null}.
074     * @param optional A flag whether the dependency is optional or mandatory, may be {@code null}.
075     * @param exclusions The exclusions that apply to transitive dependencies, may be {@code null} if none.
076     */
077    public Dependency(Artifact artifact, String scope, Boolean optional, Collection<Exclusion> exclusions) {
078        this(artifact, scope, Exclusions.copy(exclusions), optional);
079    }
080
081    private Dependency(Artifact artifact, String scope, Set<Exclusion> exclusions, Boolean optional) {
082        // NOTE: This constructor assumes immutability of the provided exclusion collection, for internal use only
083        this.artifact = requireNonNull(artifact, "artifact cannot be null");
084        this.scope = (scope != null) ? scope : "";
085        this.optional = optional;
086        this.exclusions = exclusions;
087    }
088
089    /**
090     * Gets the artifact being depended on.
091     *
092     * @return The artifact, never {@code null}.
093     */
094    public Artifact getArtifact() {
095        return artifact;
096    }
097
098    /**
099     * Sets the artifact being depended on.
100     *
101     * @param artifact The artifact, must not be {@code null}.
102     * @return The new dependency, never {@code null}.
103     */
104    public Dependency setArtifact(Artifact artifact) {
105        if (this.artifact.equals(artifact)) {
106            return this;
107        }
108        return new Dependency(artifact, scope, exclusions, optional);
109    }
110
111    /**
112     * Gets the scope of the dependency. The scope defines in which context this dependency is relevant.
113     *
114     * @return The scope or an empty string if not set, never {@code null}.
115     */
116    public String getScope() {
117        return scope;
118    }
119
120    /**
121     * Sets the scope of the dependency, e.g. "compile".
122     *
123     * @param scope The scope of the dependency, may be {@code null}.
124     * @return The new dependency, never {@code null}.
125     */
126    public Dependency setScope(String scope) {
127        if (this.scope.equals(scope) || (scope == null && this.scope.isEmpty())) {
128            return this;
129        }
130        return new Dependency(artifact, scope, exclusions, optional);
131    }
132
133    /**
134     * Indicates whether this dependency is optional or not. Optional dependencies can be ignored in some contexts.
135     *
136     * @return {@code true} if the dependency is (definitively) optional, {@code false} otherwise.
137     */
138    public boolean isOptional() {
139        return Boolean.TRUE.equals(optional);
140    }
141
142    /**
143     * Gets the optional flag for the dependency. Note: Most clients will usually call {@link #isOptional()} to
144     * determine the optional flag, this method is for advanced use cases where three-valued logic is required.
145     *
146     * @return The optional flag or {@code null} if unspecified.
147     */
148    public Boolean getOptional() {
149        return optional;
150    }
151
152    /**
153     * Sets the optional flag for the dependency.
154     *
155     * @param optional {@code true} if the dependency is optional, {@code false} if the dependency is mandatory, may be
156     *            {@code null} if unspecified.
157     * @return The new dependency, never {@code null}.
158     */
159    public Dependency setOptional(Boolean optional) {
160        if (Objects.equals(this.optional, optional)) {
161            return this;
162        }
163        return new Dependency(artifact, scope, exclusions, optional);
164    }
165
166    /**
167     * Gets the exclusions for this dependency. Exclusions can be used to remove transitive dependencies during
168     * resolution.
169     *
170     * @return The (read-only) exclusions, never {@code null}.
171     */
172    public Collection<Exclusion> getExclusions() {
173        return exclusions;
174    }
175
176    /**
177     * Sets the exclusions for the dependency.
178     *
179     * @param exclusions The exclusions, may be {@code null}.
180     * @return The new dependency, never {@code null}.
181     */
182    public Dependency setExclusions(Collection<Exclusion> exclusions) {
183        if (hasEquivalentExclusions(exclusions)) {
184            return this;
185        }
186        return new Dependency(artifact, scope, optional, exclusions);
187    }
188
189    private boolean hasEquivalentExclusions(Collection<Exclusion> exclusions) {
190        if (exclusions == null || exclusions.isEmpty()) {
191            return this.exclusions.isEmpty();
192        }
193        if (exclusions instanceof Set) {
194            return this.exclusions.equals(exclusions);
195        }
196        return exclusions.size() >= this.exclusions.size()
197                && this.exclusions.containsAll(exclusions)
198                && exclusions.containsAll(this.exclusions);
199    }
200
201    @Override
202    public String toString() {
203        return getArtifact() + " (" + getScope() + (isOptional() ? "?" : "") + ")";
204    }
205
206    @Override
207    public boolean equals(Object obj) {
208        if (obj == this) {
209            return true;
210        } else if (obj == null || !getClass().equals(obj.getClass())) {
211            return false;
212        }
213
214        Dependency that = (Dependency) obj;
215
216        return Objects.equals(artifact, that.artifact)
217                && Objects.equals(scope, that.scope)
218                && Objects.equals(optional, that.optional)
219                && Objects.equals(exclusions, that.exclusions);
220    }
221
222    @Override
223    public int hashCode() {
224        int hash = 17;
225        hash = hash * 31 + artifact.hashCode();
226        hash = hash * 31 + scope.hashCode();
227        hash = hash * 31 + (optional != null ? optional.hashCode() : 0);
228        hash = hash * 31 + exclusions.size();
229        return hash;
230    }
231
232    private static class Exclusions extends AbstractSet<Exclusion> {
233
234        private final Exclusion[] exclusions;
235
236        public static Set<Exclusion> copy(Collection<Exclusion> exclusions) {
237            if (exclusions == null || exclusions.isEmpty()) {
238                return Collections.emptySet();
239            }
240            return new Exclusions(exclusions);
241        }
242
243        private Exclusions(Collection<Exclusion> exclusions) {
244            if (exclusions.size() > 1 && !(exclusions instanceof Set)) {
245                exclusions = new LinkedHashSet<>(exclusions);
246            }
247            this.exclusions = exclusions.toArray(new Exclusion[0]);
248        }
249
250        @Override
251        public Iterator<Exclusion> iterator() {
252            return new Iterator<Exclusion>() {
253
254                private int cursor = 0;
255
256                public boolean hasNext() {
257                    return cursor < exclusions.length;
258                }
259
260                public Exclusion next() {
261                    try {
262                        Exclusion exclusion = exclusions[cursor];
263                        cursor++;
264                        return exclusion;
265                    } catch (IndexOutOfBoundsException e) {
266                        throw new NoSuchElementException();
267                    }
268                }
269
270                public void remove() {
271                    throw new UnsupportedOperationException();
272                }
273            };
274        }
275
276        @Override
277        public int size() {
278            return exclusions.length;
279        }
280    }
281}