Repository: GradleUp/shadow (kotlin)

Issue:
Exclude a specific class from a dependency but it's the same name as a local one
### Description

I have a situation where I have a dependency with a specific file: ortus.boxlang.web.bifs.GetPageContext
In my local project I have the same file which is the one I want to override.  However, the one in the dependency always wins.  Is there a way to explicitly exclude the file from the implementation dependency but NOT the local one.  

Dependency:
```
implementation files( 'src/test/resources/libs/boxlang-web-support-' + boxlangVersion + '.jar' )
```
Include the file `ortus.boxlang.web.bifs.GetPageContext`

My local project has the same file as I want to override it.

in my shadow jar task

```
shadowJar {
	archiveClassifier = ""
	destinationDirectory = file( "build/distributions" )
	mergeServiceFiles()
	exclude "schema/**"
	exclude "org/antlr/v4/gui/**"
	exclude "org/antlr/v4/gui/**"
	exclude "com/ibm/icu/**"
	exclude 'ortus/boxlang/web/bifs/GetPageContext.class'
}
```

I use the `exclude` but it excludes my local one too.  How can I override a class in a dependency jar.

Pull request description:
- Follow up #1349.
- Closes #1357.

Patch:
diff --git a/api/shadow.api b/api/shadow.api
index 379e15eb4..c354fc0a7 100644
--- a/api/shadow.api
+++ b/api/shadow.api
@@ -203,6 +203,7 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar
 	protected abstract fun getArchiveOperations ()Lorg/gradle/api/file/ArchiveOperations;
 	public fun getConfigurations ()Lorg/gradle/api/provider/SetProperty;
 	public fun getDependencyFilter ()Lorg/gradle/api/provider/Property;
+	public fun getDuplicatesStrategy ()Lorg/gradle/api/file/DuplicatesStrategy;
 	public fun getEnableAutoRelocation ()Lorg/gradle/api/provider/Property;
 	public fun getExcludes ()Ljava/util/Set;
 	public fun getFailOnDuplicateEntries ()Lorg/gradle/api/provider/Property;
diff --git a/docs/configuration/merging/README.md b/docs/configuration/merging/README.md
index c79c20cfe..3c6405237 100644
--- a/docs/configuration/merging/README.md
+++ b/docs/configuration/merging/README.md
@@ -423,17 +423,23 @@ Different strategies will lead to different results for `foo/bar` files in the J
     }
     ```
 
-The [`ServiceFileTransformer`][ServiceFileTransformer] will not work as expected because the `duplicatesStrategy` will
-exclude the duplicate service files beforehand. However, this behavior might be what you expected for duplicate
+The [`ResourceTransformer`][ResourceTransformer]s like [`ServiceFileTransformer`][ServiceFileTransformer] will not work
+as expected because the `duplicatesStrategy` will exclude the duplicate service files beforehand. However, this behavior might be what you expected for duplicate
 `foo/bar` files, preventing them from being included.
-Want `ResourceTransformer`s and `duplicatesStrategy` to work together? There is a way to achieve this, leave the
-`duplicatesStrategy` as `INCLUDE` and declare a custom [`ResourceTransformer`][ResourceTransformer] to handle the
-duplicate files.
-If you just want to keep the current behavior and preserve the first found resource, there is a simple built-in one to
-handle this called [`PreserveFirstFoundResourceTransformer`][PreserveFirstFoundResourceTransformer].
+
+Want [`ResourceTransformer`][ResourceTransformer]s and `duplicatesStrategy` to work together? There are several ways to
+do it:
+
+- Use [`eachFile`][Jar.eachFile] or [`filesMatching`][Jar.filesMatching] to override the strategy for specific files.
+- Keep `duplicatesStrategy = INCLUDE` and write your own [`ResourceTransformer`][ResourceTransformer] to handle duplicates.
+
+If you just want to keep the current behavior and preserve the first found resources, there is a simple built-in one
+called [`PreserveFirstFoundResourceTransformer`][PreserveFirstFoundResourceTransformer].
 
 
 [AbstractCopyTask]: https://docs.gradle.org/current/dsl/org.gradle.api.tasks.AbstractCopyTask.html
+[Jar.eachFile]: https://docs.gradle.org/current/dsl/org.gradle.jvm.tasks.Jar.html#org.gradle.jvm.tasks.Jar:eachFile(groovy.lang.Closure)
+[Jar.filesMatching]: https://docs.gradle.org/current/dsl/org.gradle.jvm.tasks.Jar.html#org.gradle.jvm.tasks.Jar:filesMatching(java.lang.Iterable,%20org.gradle.api.Action)
 [AppendingTransformer]: ../../api/shadow/com.github.jengelman.gradle.plugins.shadow.transformers/-appending-transformer/index.html
 [DuplicatesStrategy]: https://docs.gradle.org/current/javadoc/org/gradle/api/file/DuplicatesStrategy.html
 [GroovyExtensionModuleTransformer]: ../../api/shadow/com.github.jengelman.gradle.plugins.shadow.transformers/-groovy-extension-module-transformer/index.html
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt
index 2d94a175d..29e34db9b 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt
@@ -159,9 +159,9 @@ public abstract class ShadowJar :
   /**
    * Fails build if the ZIP entries in the shadowed JAR are duplicate.
    *
-   * This is related to setting [duplicatesStrategy] to [DuplicatesStrategy.FAIL] but there are some differences:
+   * This is related to setting [getDuplicatesStrategy] to [DuplicatesStrategy.FAIL] but there are some differences:
    * - It only checks the entries in the shadowed jar, not the input files.
-   * - It works with setting [duplicatesStrategy] to any value.
+   * - It works with setting [getDuplicatesStrategy] to any value.
    * - It provides a more strict check before the JAR is created.
    *
    * Defaults to `false`.
@@ -179,6 +179,36 @@ public abstract class ShadowJar :
   @Input // Trigger task executions after excludes changed.
   override fun getExcludes(): MutableSet<String> = super.getExcludes()
 
+  /**
+   * Returns the strategy to use when trying to copy more than one file to the same destination.
+   *
+   * This strategy can be overridden for individual files by using [eachFile] or [filesMatching].
+   *
+   * The default value is [DuplicatesStrategy.INCLUDE]. Different strategies will lead to different results for
+   * `foo/bar` files in the JARs to be merged:
+   * - [DuplicatesStrategy.EXCLUDE]: The **first** `foo/bar` file will be included in the final JAR.
+   * - [DuplicatesStrategy.FAIL]: **Fail** the build with a `DuplicateFileCopyingException` if there are duplicate `foo/bar` files.
+   * - [DuplicatesStrategy.INCLUDE]: The **last** `foo/bar` file will be included in the final JAR (the default behavior).
+   * - [DuplicatesStrategy.INHERIT]: **Fail** the build with an exception like `Entry .* is a duplicate but no duplicate handling strategy has been set`.
+   * - [DuplicatesStrategy.WARN]: The **last** `foo/bar` file will be included in the final JAR, and a warning message will be logged.
+   *
+   * **NOTE:** The strategy takes precedence over transforming and relocating.
+   * Some [ResourceTransformer]s like [ServiceFileTransformer] will not work as expected with setting the strategy to
+   * [DuplicatesStrategy.EXCLUDE], as the service files are excluded beforehand. Want [ResourceTransformer]s and the
+   * strategy to work together? There are several ways to do it:
+   * - Use [eachFile] or [filesMatching] to override the strategy for specific files.
+   * - Keep `duplicatesStrategy = INCLUDE` and write your own [ResourceTransformer] to handle duplicates.
+   *
+   * If you just want to keep the current behavior and preserve the first found resources, there is a simple built-in one
+   * called [com.github.jengelman.gradle.plugins.shadow.transformers.PreserveFirstFoundResourceTransformer].
+   *
+   * @see [eachFile]
+   * @see [filesMatching]
+   * @see [DuplicatesStrategy]
+   * @see [org.gradle.api.file.CopySpec.getDuplicatesStrategy]
+   */
+  override fun getDuplicatesStrategy(): DuplicatesStrategy = super.getDuplicatesStrategy()
+
   @get:Inject
   protected abstract val archiveOperations: ArchiveOperations
 
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/PreserveFirstFoundResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/PreserveFirstFoundResourceTransformer.kt
index f9e10cff6..416bf2961 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/PreserveFirstFoundResourceTransformer.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/PreserveFirstFoundResourceTransformer.kt
@@ -2,6 +2,7 @@ package com.github.jengelman.gradle.plugins.shadow.transformers
 
 import com.github.jengelman.gradle.plugins.shadow.internal.setProperty
 import javax.inject.Inject
+import org.gradle.api.file.DuplicatesStrategy
 import org.gradle.api.file.FileTreeElement
 import org.gradle.api.model.ObjectFactory
 import org.gradle.api.provider.SetProperty
@@ -14,6 +15,9 @@ import org.gradle.api.tasks.Internal
  * This is useful when you set `shadowJar.duplicatesStrategy = DuplicatesStrategy.INCLUDE` (the default behavior) and
  * want to ensure that only the first found resource is included in the final JAR. If there are multiple resources with
  * the same path in a project and its dependencies, the first one found should be the projects'.
+ *
+ * @see [DuplicatesStrategy]
+ * @see [com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.getDuplicatesStrategy]
  */
 @CacheableTransformer
 public open class PreserveFirstFoundResourceTransformer @Inject constructor(


Test patch:
diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/caching/ShadowJarCachingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/caching/ShadowJarCachingTest.kt
index 85049a751..9979080bc 100644
--- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/caching/ShadowJarCachingTest.kt
+++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/caching/ShadowJarCachingTest.kt
@@ -117,12 +117,13 @@ class ShadowJarCachingTest : BaseCachingTest() {
       DuplicatesStrategy.INCLUDE,
       DuplicatesStrategy.WARN,
     ).forEach { strategy ->
-      projectScriptPath.appendText(
-        """
-          $shadowJar {
-            duplicatesStrategy = DuplicatesStrategy.$strategy
-          }
-        """.trimIndent() + System.lineSeparator(),
+      projectScriptPath.writeText(
+        getDefaultProjectBuildScript(withGroup = true, withVersion = true) +
+          """
+            $shadowJar {
+              duplicatesStrategy = DuplicatesStrategy.$strategy
+            }
+          """.trimIndent(),
       )
 
       assertCompositeExecutions()
diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformerTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformerTest.kt
index 5acfdf58b..2e20e5099 100644
--- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformerTest.kt
+++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformerTest.kt
@@ -224,6 +224,48 @@ class ServiceFileTransformerTest : BaseTransformerTest() {
     }
   }
 
+  @Test
+  fun strategyExcludeCanBeOverriddenByEachFile() {
+    writeDuplicatesStrategy(DuplicatesStrategy.EXCLUDE)
+    projectScriptPath.appendText(
+      """
+        $shadowJar {
+          eachFile {
+            duplicatesStrategy = DuplicatesStrategy.INCLUDE
+          }
+        }
+      """.trimIndent(),
+    )
+
+    run(shadowJarTask)
+
+    assertThat(outputShadowJar).useAll {
+      getContent(ENTRY_SERVICES_SHADE).isEqualTo(CONTENT_ONE_TWO)
+      getContent(ENTRY_SERVICES_FOO).isEqualTo("one\ntwo")
+    }
+  }
+
+  @Test
+  fun strategyExcludeCanBeOverriddenByFileMatching() {
+    writeDuplicatesStrategy(DuplicatesStrategy.EXCLUDE)
+    projectScriptPath.appendText(
+      """
+        $shadowJar {
+          filesMatching('$ENTRY_SERVICES_SHADE') {
+            duplicatesStrategy = DuplicatesStrategy.INCLUDE
+          }
+        }
+      """.trimIndent(),
+    )
+
+    run(shadowJarTask)
+
+    assertThat(outputShadowJar).useAll {
+      getContent(ENTRY_SERVICES_SHADE).isEqualTo(CONTENT_ONE_TWO)
+      getContent(ENTRY_SERVICES_FOO).isEqualTo("one")
+    }
+  }
+
   private fun writeDuplicatesStrategy(strategy: DuplicatesStrategy) {
     projectScriptPath.appendText(
       """
@@ -234,7 +276,7 @@ class ServiceFileTransformerTest : BaseTransformerTest() {
           duplicatesStrategy = DuplicatesStrategy.$strategy
           mergeServiceFiles()
         }
-      """.trimIndent(),
+      """.trimIndent() + System.lineSeparator(),
     )
   }