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:
Closes #1357.

---

- [x] [CHANGELOG](https://github.com/GradleUp/shadow/blob/main/docs/changes/README.md)'s "Unreleased" section has been updated, if applicable.

Patch:
diff --git a/api/shadow.api b/api/shadow.api
index ec31a39a7..2dce33b7a 100644
--- a/api/shadow.api
+++ b/api/shadow.api
@@ -394,6 +394,18 @@ public class com/github/jengelman/gradle/plugins/shadow/transformers/ManifestRes
 	public fun transform (Lcom/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext;)V
 }
 
+public class com/github/jengelman/gradle/plugins/shadow/transformers/PreserveFirstFoundResourceTransformer : com/github/jengelman/gradle/plugins/shadow/transformers/ResourceTransformer {
+	public fun <init> (Lorg/gradle/api/model/ObjectFactory;)V
+	public fun canTransformResource (Lorg/gradle/api/file/FileTreeElement;)Z
+	protected final fun getFound ()Ljava/util/Set;
+	public fun getName ()Ljava/lang/String;
+	public final fun getObjectFactory ()Lorg/gradle/api/model/ObjectFactory;
+	public fun getResources ()Lorg/gradle/api/provider/SetProperty;
+	public fun hasTransformedResource ()Z
+	public fun modifyOutputStream (Lorg/apache/tools/zip/ZipOutputStream;Z)V
+	public fun transform (Lcom/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext;)V
+}
+
 public class com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer : com/github/jengelman/gradle/plugins/shadow/transformers/ResourceTransformer {
 	public fun <init> (Lorg/gradle/api/model/ObjectFactory;)V
 	public fun canTransformResource (Lorg/gradle/api/file/FileTreeElement;)Z
diff --git a/docs/changes/README.md b/docs/changes/README.md
index de6e57592..a489f29fc 100644
--- a/docs/changes/README.md
+++ b/docs/changes/README.md
@@ -3,6 +3,12 @@
 
 ## [Unreleased](https://github.com/GradleUp/shadow/compare/9.0.0-rc2...HEAD) - 2025-xx-xx
 
+### Added
+
+- Add `PreserveFirstFoundResourceTransformer`. ([#1548](https://github.com/GradleUp/shadow/pull/1548))  
+  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.
+
 ### Changed
 
 - **BREAKING CHANGE:** Rename `ShadowJar`'s `enableRelocation` to `enableAutoRelocation`. ([#1541](https://github.com/GradleUp/shadow/pull/1541))  
diff --git a/docs/configuration/merging/README.md b/docs/configuration/merging/README.md
index 74f37dfc9..11105853d 100644
--- a/docs/configuration/merging/README.md
+++ b/docs/configuration/merging/README.md
@@ -429,7 +429,8 @@ exclude the duplicated service files beforehand. However, this behavior might be
 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
 duplicated 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].
 
 
 [AbstractCopyTask]: https://docs.gradle.org/current/dsl/org.gradle.api.tasks.AbstractCopyTask.html
@@ -440,6 +441,7 @@ duplicated files.
 [Log4j2PluginsCacheFileTransformer]: ../../api/shadow/com.github.jengelman.gradle.plugins.shadow.transformers/-log4j2-plugins-cache-file-transformer/index.html
 [ResourceTransformer]: ../../api/shadow/com.github.jengelman.gradle.plugins.shadow.transformers/-resource-transformer/index.html
 [ServiceFileTransformer]: ../../api/shadow/com.github.jengelman.gradle.plugins.shadow.transformers/-service-file-transformer/index.html
+[PreserveFirstFoundResourceTransformer]: ../../api/shadow/com.github.jengelman.gradle.plugins.shadow.transformers/-preserve-first-found-resource-transformer/index.html
 [ShadowJar.append]: ../../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/append.html
 [ShadowJar.from]: https://docs.gradle.org/current/dsl/org.gradle.jvm.tasks.Jar.html#org.gradle.jvm.tasks.Jar:from(java.lang.Object,%20org.gradle.api.Action)
 [ShadowJar.transform]: ../../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/transform.html
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
new file mode 100644
index 000000000..f9e10cff6
--- /dev/null
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/PreserveFirstFoundResourceTransformer.kt
@@ -0,0 +1,32 @@
+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.FileTreeElement
+import org.gradle.api.model.ObjectFactory
+import org.gradle.api.provider.SetProperty
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.Internal
+
+/**
+ * A resource processor that preserves the first resource matched and excludes all others.
+ *
+ * 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'.
+ */
+@CacheableTransformer
+public open class PreserveFirstFoundResourceTransformer @Inject constructor(
+  final override val objectFactory: ObjectFactory,
+) : ResourceTransformer by ResourceTransformer.Companion {
+  @get:Internal
+  protected val found: MutableSet<String> = mutableSetOf()
+
+  @get:Input
+  public open val resources: SetProperty<String> = objectFactory.setProperty()
+
+  override fun canTransformResource(element: FileTreeElement): Boolean {
+    val path = element.path
+    return resources.get().contains(path) && !found.add(path)
+  }
+}


Test patch:
diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt
index 0d81d0f92..3e00deb79 100644
--- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt
+++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt
@@ -153,6 +153,39 @@ class TransformersTest : BaseTransformerTest() {
     }
   }
 
+  @Test
+  fun canPreserveFirstFoundResource() {
+    path("src/main/resources/foo/bar").writeText("bar1")
+    path("src/main/resources/foo/baz").writeText("baz1")
+    val one = buildJarOne {
+      insert("foo/bar", "bar2")
+      insert("foo/baz", "baz2")
+    }
+    val two = buildJarTwo {
+      insert("foo/bar", "bar3")
+      insert("foo/baz", "baz3")
+    }
+    projectScriptPath.appendText(
+      transform<PreserveFirstFoundResourceTransformer>(
+        dependenciesBlock = implementationFiles(one, two),
+        transformerBlock = "resources = ['foo/bar']",
+      ),
+    )
+
+    run(shadowJarTask)
+
+    assertThat(outputShadowJar).useAll {
+      containsOnly(
+        "foo/",
+        "foo/bar",
+        "foo/baz",
+        *manifestEntries,
+      )
+      getContent("foo/bar").isEqualTo("bar1")
+      getContent("foo/baz").isEqualTo("baz3")
+    }
+  }
+
   @Test
   fun canUseCustomTransformer() {
     projectScriptPath.appendText(