// Copyright (C) 2023 Gradle, Inc.
//
// Licensed under the Creative Commons Attribution-Noncommercial-ShareAlike 4.0 International License.;
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      https://creativecommons.org/licenses/by-nc-sa/4.0/
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

[[downgrading_versions_and_excluding_dependencies]]
= Downgrading versions and excluding dependencies

[[sec:enforcing_dependency_version]]
== Overriding transitive dependency versions

Gradle resolves any dependency version conflicts by selecting the latest version found in the dependency graph.
Some projects might need to divert from the default behavior and enforce an earlier version of a dependency e.g. if the source code of the project depends on an older API of a dependency than some of the external libraries.

[WARNING]
====
Forcing a version of a dependency requires a conscious decision.
Changing the version of a transitive dependency might lead to runtime errors if external libraries do not properly function without them.
Consider upgrading your source code to use a newer version of the library as an alternative approach.
====

In general, forcing dependencies is done to downgrade a dependency.
There might be different use cases for downgrading:

- a bug was discovered in the latest release
- your code depends on a lower version which is not binary compatible
- your code doesn't depend on the code paths which need a higher version of a dependency

In all situations, this is best expressed saying that your code _strictly depends on_ a version of a transitive.
Using <<rich_versions.adoc#sec:strict-version,strict versions>>, you will effectively depend on the version you declare, even if a transitive dependency says otherwise.

[NOTE]
====
Strict dependencies are to some extent similar to Maven's _nearest first_ strategy, but there are subtle differences:

- _strict dependencies_ don't suffer an ordering problem: they are applied transitively to the subgraph, and it doesn't matter in which order dependencies are declared.
- conflicting strict dependencies will trigger a build failure that you have to resolve
- strict dependencies can be used with rich versions, meaning that <<#sec:strict-version-consequences,it's better to express the requirement in terms of a _strict range_ combined with a single _preferred version_>>.
====

Let's say a project uses the link:https://hc.apache.org/httpcomponents-client-ga/[HttpClient library] for performing HTTP calls. HttpClient pulls in link:https://commons.apache.org/proper/commons-codec/[Commons Codec] as transitive dependency with version 1.10.
However, the production source code of the project requires an API from Commons Codec 1.9 which is not available in 1.10 anymore.
A dependency version can be enforced by declaring it as strict it in the build script:

.Setting a strict version
====
include::sample[dir="snippets/dependencyManagement/managingTransitiveDependencies-forceForDependency/kotlin",files="build.gradle.kts[tags=force-using-strictly]"]
include::sample[dir="snippets/dependencyManagement/managingTransitiveDependencies-forceForDependency/groovy",files="build.gradle[tags=force-using-strictly]"]
====

[[sec:strict-version-consequences]]
=== Consequences of using strict versions

Using a strict version must be carefully considered, in particular by library authors.
As the _producer_, a strict version will effectively behave like a _force_: the version declaration takes precedence over whatever is found in the transitive dependency graph.
In particular, a _strict version_ will override any other _strict version_ on the same module found transitively.

However, for consumers, strict versions are still considered globally during graph resolution and _may trigger an error_ if the consumer disagrees.

For example, imagine that your project `B` _strictly_ depends on `C:1.0`.
Now, a consumer, `A`, depends on both `B` and `C:1.1`.

Then this would trigger a resolution error because `A` says it needs `C:1.1` but `B`, _within its subgraph_, strictly needs `1.0`.
This means that if you choose a _single version_ in a strict constraint, then the version can _no longer be upgraded_, unless the consumer also sets a strict version constraint on the same module.

In the example above, `A` would have to say it _strictly depends on 1.1_.

For this reason, a good practice is that if you use _strict versions_, you should express them in terms of ranges and a preferred version within this range.
For example, `B` might say, instead of `strictly 1.0`, that it _strictly depends_ on the `[1.0, 2.0[` range, but _prefers_ `1.0`.
Then if a consumer chooses 1.1 (or any other version in the range), the build will _no longer fail_ (constraints are resolved).

[[forced_dependencies_vs_strict_dependencies]]
=== Forced dependencies vs strict dependencies

If the project requires a specific version of a dependency at the configuration-level this can be achieved by calling the method link:{groovyDslPath}/org.gradle.api.artifacts.ResolutionStrategy.html#org.gradle.api.artifacts.ResolutionStrategy:force(java.lang.Object++[]++)[ResolutionStrategy.force(java.lang.Object++[]++)].

.Enforcing a dependency version on the configuration-level
====
include::sample[dir="snippets/dependencyManagement/managingTransitiveDependencies-forceForConfiguration/kotlin",files="build.gradle.kts[tags=force-per-configuration]"]
include::sample[dir="snippets/dependencyManagement/managingTransitiveDependencies-forceForConfiguration/groovy",files="build.gradle[tags=force-per-configuration]"]
====

[[sec:excluding-transitive-deps]]
== Excluding transitive dependencies

While the previous section showed how you can enforce a certain version of a transitive dependency, this section covers _excludes_ as a way to remove a transitive dependency completely.

[WARNING]
====
Similar to forcing a version of a dependency, excluding a dependency completely requires a conscious decision.
Excluding a transitive dependency might lead to runtime errors if external libraries do not properly function without them.
If you use excludes, make sure that you do not utilise any code path requiring the excluded dependency by sufficient test coverage.
====

Transitive dependencies can be excluded on the level of a declared dependency.
Exclusions are spelled out as a key/value pair via the attributes `group` and/or `module` as shown in the example below.
For more information, refer to link:{javadocPath}/org/gradle/api/artifacts/ModuleDependency.html#exclude-java.util.Map-[ModuleDependency.exclude(java.util.Map)].

.Excluding a transitive dependency for a particular dependency declaration
====
include::sample[dir="snippets/dependencyManagement/managingTransitiveDependencies-excludeForDependency/kotlin",files="build.gradle.kts[tags=exclude-transitive-dependencies-1]"]
include::sample[dir="snippets/dependencyManagement/managingTransitiveDependencies-excludeForDependency/groovy",files="build.gradle[tags=exclude-transitive-dependencies-1]"]
====

In this example, we add a dependency to `commons-beanutils` but exclude the transitive dependency `commons-collections`.
In our code, shown below, we only use one method from the beanutils library, `PropertyUtils.setSimpleProperty()`.
Using this method for existing setters does not require any functionality from `commons-collections` as we verified through test coverage.

.Using a utility from the beanutils library
====
include::sample[dir="snippets/dependencyManagement/managingTransitiveDependencies-excludeForDependency/groovy",files="src/main/java/Main.java"]
====

Effectively, we are expressing that we only use a _subset_ of the library, which does not require the `commons-collection` library.
This can be seen as implicitly defining a <<feature_variants.adoc#feature_variants,feature variant>> that has not been explicitly declared by `commons-beanutils` itself.
However, the risk of breaking an untested code path increased by doing this.

For example, here we use the `setSimpleProperty()` method to modify properties defined by setters in the `Person` class, which works fine.
If we would attempt to set a property not existing on the class, we _should_ get an error like `Unknown property on class Person`.
However, because the error handling path uses a class from `commons-collections`, the error we now get is `NoClassDefFoundError: org/apache/commons/collections/FastHashMap`.
So if our code would be more dynamic, and we would forget to cover the error case sufficiently, consumers of our library might be confronted with unexpected errors.

This is only an example to illustrate potential pitfalls.
In practice, larger libraries or frameworks can bring in a huge set of dependencies.
If those libraries fail to declare features separately and can only be consumed in a "all or nothing" fashion, excludes can be a valid method to reduce the library to the feature set actually required.

On the upside, Gradle's exclude handling is, in contrast to Maven, taking the whole dependency graph into account.
So if there are multiple dependencies on a library, excludes are only exercised if all dependencies agree on them.
For example, if we add `opencsv` as another dependency to our project above, which also depends on `commons-beanutils`, `commons-collection` is no longer excluded as `opencsv` itself does *not* exclude it.


.Excludes only apply if all dependency declarations agree on an exclude
====
include::sample[dir="snippets/dependencyManagement/managingTransitiveDependencies-excludeForDependency/kotlin",files="build.gradle.kts[tags=exclude-transitive-dependencies-2]"]
include::sample[dir="snippets/dependencyManagement/managingTransitiveDependencies-excludeForDependency/groovy",files="build.gradle[tags=exclude-transitive-dependencies-2]"]
====

If we still want to have `commons-collections` excluded, because our combined usage of `commons-beanutils` and `opencsv` does not need it, we need to exclude it from the transitive dependencies of `opencsv` as well.

.Excluding a transitive dependency for multiple dependency declaration
====
include::sample[dir="snippets/dependencyManagement/managingTransitiveDependencies-excludeForDependency/kotlin",files="build.gradle.kts[tags=exclude-transitive-dependencies-3]"]
include::sample[dir="snippets/dependencyManagement/managingTransitiveDependencies-excludeForDependency/groovy",files="build.gradle[tags=exclude-transitive-dependencies-3]"]
====

Historically, excludes were also used as a band aid to fix other issues not supported by some dependency management systems.
Gradle however, offers a variety of features that might be better suited to solve a certain use case.
You may consider to look into the following features:

- <<#sec:enforcing_dependency_version,Update>> or <<#sec:enforcing_dependency_version,downgrade>> dependency versions:
  If versions of dependencies clash, it is usually better to adjust the version through a dependency constraint, instead of attempting to exclude the dependency with the undesired version.
- <<component_metadata_rules.adoc#sec:component_metadata_rules,Component Metadata Rules>>:
  If a library's metadata is clearly wrong, for example if it includes a compile time dependency which is never needed at compile time, a possible solution is to remove the dependency in a component metadata rule.
  By this, you tell Gradle that a dependency between two modules is never needed — i.e. the metadata was wrong — and therefore should *never* be considered.
  If you are developing a library, you have to be aware that this information is not published, and so sometimes an _exclude_ can be the better alternative.
- <<dependency_capability_conflict.adoc#sec:handling-mutually-exclusive-deps,Resolving mutually exclusive dependency conflicts>>:
  Another situation that you often see solved by excludes is that two dependencies cannot be used together because they represent two implementations of the same thing (the same <<dependency_capability_conflict.adoc#sub:capabilities,capability>>).
  Some popular examples are clashing logging API implementations (like `log4j` and `log4j-over-slf4j`) or modules that have different coordinates in different versions (like `com.google.collections` and `guava`).
  In these cases, if this information is not known to Gradle, it is recommended to add the missing capability information via component metadata rules as described in the <<dependency_capability_conflict.adoc#sub:declaring-component-capabilities,declaring component capabilities>> section.
  Even if you are developing a library, and your consumers will have to deal with resolving the conflict again, it is often the right solution to leave the decision to the final consumers of libraries.
  I.e. you as a library author should not have to decide which logging implementation your consumers use in the end.
