[gradle-shadow-plugin] 01/11: upstream 1.2.3
Alastair McKinstry
mckinstry at moszumanska.debian.org
Wed Oct 25 15:44:29 UTC 2017
This is an automated email from the git hooks/post-receive script.
mckinstry pushed a commit to branch debian/master
in repository gradle-shadow-plugin.
commit f99ce882c176a084c4d81460893733f111cff551
Author: Alastair McKinstry <mckinstry at debian.org>
Date: Thu Feb 18 15:53:47 2016 +0000
upstream 1.2.3
---
ChangeLog.md | 144 +++++
LICENSE | 202 ++++++
NOTICE | 5 +
README.md | 378 +++++++++++
README_old.md | 176 +++++
build.gradle | 89 +++
circle.yml | 12 +
gradle/docs.gradle | 54 ++
gradle/publish.gradle | 141 +++++
gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes
gradle/wrapper/gradle-wrapper.properties | 6 +
gradlew | 160 +++++
gradlew.bat | 90 +++
settings.gradle | 1 +
src/docs/asciidoc/index.adoc | 81 +++
.../plugins/shadow/ShadowApplicationPlugin.groovy | 149 +++++
.../gradle/plugins/shadow/ShadowBasePlugin.groovy | 25 +
.../gradle/plugins/shadow/ShadowExtension.groovy | 13 +
.../gradle/plugins/shadow/ShadowJavaPlugin.groovy | 93 +++
.../gradle/plugins/shadow/ShadowPlugin.groovy | 20 +
.../gradle/plugins/shadow/ShadowStats.groovy | 60 ++
.../plugins/shadow/impl/RelocatorRemapper.groovy | 111 ++++
.../shadow/internal/DefaultDependencyFilter.groovy | 128 ++++
.../shadow/internal/DefaultZipCompressor.groovy | 45 ++
.../shadow/internal/DependencyFilter.groovy | 76 +++
.../shadow/internal/GradleVersionUtil.groovy | 62 ++
.../plugins/shadow/internal/JavaJarExec.groovy | 21 +
.../shadow/internal/StartScriptGenerator.groovy | 140 ++++
.../plugins/shadow/internal/ZipCompressor.groovy | 25 +
.../gradle111/Gradle111DefaultZipCompressor.groovy | 31 +
.../plugins/shadow/relocation/Relocator.groovy | 42 ++
.../shadow/relocation/SimpleRelocator.groovy | 186 ++++++
.../shadow/tasks/DefaultInheritManifest.groovy | 92 +++
.../plugins/shadow/tasks/InheritManifest.groovy | 10 +
.../gradle/plugins/shadow/tasks/KnowsTask.groovy | 17 +
.../plugins/shadow/tasks/ShadowCopyAction.groovy | 425 +++++++++++++
.../shadow/tasks/ShadowCreateStartScripts.groovy | 88 +++
.../gradle/plugins/shadow/tasks/ShadowJar.java | 327 ++++++++++
.../gradle/plugins/shadow/tasks/ShadowSpec.java | 40 ++
.../ApacheLicenseResourceTransformer.groovy | 56 ++
.../ApacheNoticeResourceTransformer.groovy | 207 ++++++
.../transformers/AppendingTransformer.groovy | 67 ++
.../ComponentsXmlResourceTransformer.groovy | 183 ++++++
.../DontIncludeResourceTransformer.groovy | 58 ++
.../GroovyExtensionModuleTransformer.groovy | 109 ++++
.../transformers/IncludeResourceTransformer.groovy | 61 ++
.../ManifestResourceTransformer.groovy | 109 ++++
.../transformers/PropertiesFileTransformer.groovy | 209 ++++++
.../transformers/ServiceFileTransformer.groovy | 223 +++++++
.../plugins/shadow/transformers/Transformer.groovy | 44 ++
.../transformers/XmlAppendingTransformer.groovy | 113 ++++
.../com.github.johnrengelman.shadow.properties | 16 +
.../plugins/shadow/internal/unixStartScript.txt | 161 +++++
.../plugins/shadow/internal/windowsStartScript.txt | 89 +++
src/main/resources/shadow-version.txt | 1 +
src/main/resources/shadowBanner.txt | 34 +
.../gradle/plugins/shadow/ApplicationSpec.groovy | 194 ++++++
.../gradle/plugins/shadow/FilteringSpec.groovy | 441 +++++++++++++
.../gradle/plugins/shadow/PublishingSpec.groovy | 147 +++++
.../gradle/plugins/shadow/RelocationSpec.groovy | 315 +++++++++
.../gradle/plugins/shadow/ShadowPluginSpec.groovy | 536 ++++++++++++++++
.../gradle/plugins/shadow/TransformerSpec.groovy | 705 +++++++++++++++++++++
.../relocation/SimpleRelocatorParameterTest.groovy | 53 ++
.../shadow/relocation/SimpleRelocatorTest.groovy | 137 ++++
.../ApacheLicenseResourceTransformerTest.groovy | 60 ++
...eNoticeResourceTransformerParameterTests.groovy | 78 +++
.../ApacheNoticeResourceTransformerTest.groovy | 60 ++
.../transformers/AppendingTransformerTest.groovy | 60 ++
.../ComponentsXmlResourceTransformerTest.groovy | 60 ++
.../PropertiesFileTransformerSpec.groovy | 139 ++++
.../transformers/ServiceFileTransformerSpec.groovy | 68 ++
.../transformers/TransformerSpecSupport.groovy | 14 +
.../transformers/TransformerTestSupport.groovy | 13 +
.../XmlAppendingTransformerTest.groovy | 61 ++
.../plugins/shadow/util/AppendableJar.groovy | 25 +
.../shadow/util/AppendableMavenFileModule.groovy | 70 ++
.../util/AppendableMavenFileRepository.groovy | 15 +
.../gradle/plugins/shadow/util/JarBuilder.groovy | 51 ++
.../plugins/shadow/util/PluginSpecification.groovy | 127 ++++
.../plugins/shadow/util/file/ExecOutput.groovy | 13 +
.../gradle/plugins/shadow/util/file/Results.groovy | 58 ++
.../shadow/util/file/TestDirectoryProvider.java | 17 +
.../gradle/plugins/shadow/util/file/TestFile.java | 573 +++++++++++++++++
.../plugins/shadow/util/file/TestFileHelper.groovy | 203 ++++++
.../util/file/TestNameTestDirectoryProvider.java | 114 ++++
.../shadow/util/file/TestWorkspaceBuilder.groovy | 39 ++
.../plugins/shadow/util/repo/AbstractModule.groovy | 81 +++
.../util/repo/maven/AbstractMavenModule.groovy | 330 ++++++++++
.../util/repo/maven/DefaultMavenMetaData.groovy | 33 +
.../shadow/util/repo/maven/MavenDependency.groovy | 19 +
.../shadow/util/repo/maven/MavenFileModule.groovy | 55 ++
.../util/repo/maven/MavenFileRepository.groovy | 23 +
.../shadow/util/repo/maven/MavenMetaData.groovy | 6 +
.../shadow/util/repo/maven/MavenModule.groovy | 45 ++
.../plugins/shadow/util/repo/maven/MavenPom.groovy | 42 ++
.../shadow/util/repo/maven/MavenRepository.groovy | 12 +
.../shadow/util/repo/maven/MavenScope.groovy | 29 +
src/test/jars/plexus-utils-1.4.1.jar | Bin 0 -> 188648 bytes
src/test/jars/test-artifact-1.0-SNAPSHOT.jar | Bin 0 -> 3115 bytes
src/test/jars/test-project-1.0-SNAPSHOT.jar | Bin 0 -> 3906 bytes
src/test/resources/components-1.xml | 48 ++
src/test/resources/components-2.xml | 48 ++
src/test/resources/components-expected.xml | 55 ++
src/test/resources/junit-3.8.2.jar | Bin 0 -> 120640 bytes
src/test/resources/test-artifact-1.0-SNAPSHOT.jar | Bin 0 -> 3115 bytes
src/test/resources/test-project-1.0-SNAPSHOT.jar | Bin 0 -> 3906 bytes
106 files changed, 10777 insertions(+)
diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
index 0000000..7d8bfe3
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,144 @@
+v1.2.3
+======
+
++ Support for Gradle 2.11-rc-1 ([Issue #177](https://github.com/johnrengelman/shadow/issues/177))
++ Convert internal framework to [Gradle TestKit](https://docs.gradle.org/current/userguide/test_kit.html)
++ [Fedor Korotkov](https://github.com/fkorotkov) - Use BufferedOutputStream when writing the Zip file ([PR #171](https://github.com/johnrengelman/shadow/pull/171))
++ [Haw-Bin Chai](https://github.com/hbchai) - Quote Jar path in Windows start script as it may contain spaces ([PR #170](https://github.com/johnrengelman/shadow/pull/170))
++ [Serban Iordache](https://github.com/siordache) - Evaluate relocation specs when merging service descriptors ([PR #165](https://github.com/johnrengelman/shadow/pull/165))
+
+v1.2.2
+======
+
++ [Minecrell](https://github.com/Minecrell) Gradle 2.5 compatibility ([Issue #147](https://github.com/johnrengelman/shadow/issues/147))
+
+v1.2.1
+======
+
++ Apply package relocations to dependency resources ([Issue #114](https://github.com/johnrengelman/shadow/issues/114))
+
+v1.2.0
+======
+
++ Re-organize some code to remove need for forcing the Gradle API ClassLoader to allow the `org.apache.tools.zip` package.
++ Upgrade JDOM library from 1.1 to 2.0.5 (change dependency from `jdom:jdom:1.1` to `org.jdom:jdom2:2.0.5`) ([Issue #98](https://github.com/johnrengelman/shadow/issues/98))
++ Convert ShadowJar.groovy to ShadowJar.java to workaround binary incompatibility introduced by Gradle 2.2 ([Issue #106](https://github.com/johnrengelman/shadow/issues/106))
++ Updated ASM library to `5.0.3` to support JDK8 ([Issue #97](https://github.com/johnrengelman/shadow/issues/97))
++ Allows for regex pattern matching in the `dependency` string when including/excluding ([Issue #83](https://github.com/johnrengelman/shadow/issues/83))
++ Apply package relocations to resource files ([Issue #93](https://github.com/johnrengelman/shadow/issues/93))
+
+v1.1.2
+======
+
++ fix bug in `runShadow` where dependencies from the `shadow` configuration are not available ([Issue #94](https://github.com/johnrengelman/shadow/issues/94))
+
+v1.1.1
+======
+
++ Fix bug in `'createStartScripts'` task that was causing it to not execute `'shadowJar'` task ([Issue #90](https://github.com/johnrengelman/shadow/issues/90))
++ Do not include `null` in ShadowJar Manifest `'Class-Path'` value when `jar` task does not specify a value for it. ([Issue #92](https://github.com/johnrengelman/shadow/issues/92))
++ ShadowJar Manifest `'Class-Path'` should reference jars from `'shadow'` config as relative to location of `shadowJar` output ([Issue #91](https://github.com/johnrengelman/shadow/issues/91))
+
+v1.1.0
+======
+
++ (Breaking Change!) Fix leaking of `shadowJar.manifest` into `jar.manifest`. ([Issue #82](https://github.com/johnrengelman/shadow/issues/82))
+ To simplify behavior, the `shadowJar.appendManifest` method has been removed. Replace uses with `shadowJar.manifest`
++ `ShadowTask` now has a `configurations` property that is resolved to the files in the resolved configuration before
+ being added to the copy spec. This allows for an easier implementation for filtering. The default 'shadowJar' task
+ has the convention of adding the `'runtime'` scope to this list. Manually created instances of `ShadowTask` have no
+ configurations added by default and can be configured by setting `task.configurations`.
++ Properly configure integration with the `'maven'` plugin when added. When adding `'maven'` the `'uploadShadow'` task
+ will now properly configure the POM dependencies by removing the `'compile'` and `'runtime'` configurations from the
+ POM and adding the `'shadow'` configuration as a `RUNTIME` scope in the POM. This behavior matches the behavior when
+ using the `'maven-publish'` plugin.
++ [Matt Hurne](https://github.com/mhurne) - Allow `ServiceFileTransformer` to specify include/exclude patterns for
+ files within the configured path to merge.
++ [Matt Hurne](https://github.com/mhurne) - Added `GroovyExtensionModuleTransformer` for merging Groovy Extension module
+ descriptor files. The existing `ServiceFileTransformer` now excludes Groovy Extension Module descriptors by default.
++ `distShadowZip` and `distShadowZip` now contain the shadow library and run scripts instead of the default from the `'application'` plugin ([Issue #89](https://github.com/johnrengelman/shadow/issues/89))
+
+v1.0.3
+======
+
++ Make service files root path configurable for `ServiceFileTransformer` ([Issue #72](https://github.com/johnrengelman/shadow/issues/72))
++ [Andres Almiray](https://github.com/aalmiray) - Added PropertiesFileTransformer ([Issue #73](https://github.com/johnrengelman/shadow/issues/73))
++ [Brandon Kearby](https://github.com/brandonkearby) - Fixed StackOverflow when a cycle occurs in the resolved dependency graph ([Issue #69](https://github.com/johnrengelman/shadow/pull/69))
++ Apply Transformers to project resources ([Issue #70](https://github.com/johnrengelman/shadow/issues/70), [Issue #71](https://github.com/johnrengelman/shadow/issues/71))
++ Do not drop non-class files from dependencies when relocation is enabled. Thanks to [Minecrell](https://github.com/Minecrell) for digging into this. ([Issue #61](https://github.com/johnrengelman/shadow/issues/61))
++ Remove support for applying individual sub-plugins by Id (easier maintenance and cleaner presentation in Gradle Portal)
+
+v1.0.2
+======
+
++ Do not add an empty Class-Path attribute to the manifest when the `shadow` configuration contains no dependencies.
++ `runShadow` now registers `shadowJar` as an input. Previously, `runShadow` did not execute `shadowJar` and an error occurred.
++ Support Gradle 2.0 ([Issue #66](https://github.com/johnrengelman/shadow/issues/66))
++ Do not override existing 'Class-Path' Manifest attribute settings from Jar configuration. Instead combine. ([Issue #65](https://github.com/johnrengelman/shadow/issues/65))
+
+v1.0.1
+======
+
++ Fix issue where non-class files are dropped when using relocation ([Issue #58](https://github.com/johnrengelman/shadow/issues/58))
++ Do not create a / directory inside the output jar.
++ Fix `runShadow` task to evaluate the `shadowJar.archiveFile` property at execution time. ([Issue #60](https://github.com/johnrengelman/shadow/issues/60))
+
+v1.0.0
+======
+
++ Previously known as v0.9.0
++ All changes from 0.9.0-M1 to 0.9.0-M5
++ Properly configure the ShadowJar task inputs to observe the include/excludes from the `dependencies` block. This
+ allows UP-TO-DATE checking to work properly when changing the `dependencies` rules ([Issue #54](https://github.com/johnrengelman/shadow/issues/54))
++ Apply relocation remappings to classes and imports in source project ([Issue #55](https://github.com/johnrengelman/shadow/issues/55))
++ Do not create directories in jar for source of remapped class, created directories in jar for destination of remapped classes ([Issue #53](https://github.com/johnrengelman/shadow/issues/53))
+
+v0.9.0-M5
+=========
+
++ Add commons-io to compile classpath
++ Update asm library to 4.1
+
+v0.9.0-M4
+=========
+
++ Break plugin into multiple sub-plugins. `ShadowBasePlugin` is always applied.
+ `ShadowJavaPlugin` and `ShadowApplicationPlugin` are applied in reaction to applying the `java` and `application`
+ plugins respectively.
++ Shadow does not applied `java` plugin automatically. `java` or `groovy` must be applied in conjunction with `shadow`.
++ Moved artifact filtering to `dependencies {}` block underneath `shadowJar`. This allows better include/exclude control
+ for dependencies.
++ Dependencies added to the `shadow` configuration are automatically added to the `Class-Path` attribute in the manifest
+ for `shadowJar`
++ Applying `application` plugin and settings `mainClassName` automatically configures the `Main-Class` attribute in
+ the manifest for `shadowJar`
++ `runShadow` now utilizes the output of the `shadowJar` and executes using `java -jar <shadow jar file>`
++ Start Scripts for shadow distribution now utilize `java -jar` to execute instead of placing all files on classpath
+ and executing main class.
++ Excluding/Including dependencies no longer includes transitive dependencies. All dependencies for inclusion/exclusion
+ must be explicitly configured via a spec.
+
+v0.9.0-M3
+=========
+
++ Use commons.io FilenameUtils to determine name of resolved jars for including/excluding
+
+v0.9.0-M2
+=========
+
++ Added integration with `application` plugin to replace old `OutputSignedJars` task
++ Fixed bug that resulted in duplicate file entries in the resulting Jar
++ Changed plugin id to 'com.github.johnrengelman.shadow' to support Gradle 2.x plugin infrastructure.
+
+v0.9.0-M1
+=========
+
++ Rewrite based on Gradle Jar Task
++ `ShadowJar` now extends `Jar`
++ Removed `signedCompile` and `signedRuntime` configurations in favor of `shadow` configuration
++ Removed `OutputSignedJars` task
+
+<= v0.8
+=======
+
+See [here](README_old.md)
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..7a4a3ea
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.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.
\ No newline at end of file
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..8c8d743
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,5 @@
+Gradle-Shadow-Plugin
+Copyright (c) 2013 John Engelman All Rights Reserved.
+
+This product is licensed to you under the Apache License, Version 2.0 (the "License").
+You may not use this product except in compliance with the License.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..62031f9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,378 @@
+# Gradle Shadow
+
+Shadow is an extension of the Gradle Jar task that optimizes FatJar/UberJar creation by using JarInputStream and
+JarOutputStream to copy file contents. This avoids the unnecessary I/O overhead of expanding jar files to disk
+before recombining them. Shadow provides the similar filtering, relocation, and transformation capabilities as the
+Maven Shade plugin. Starting with version 0.9, Shadow is a complete re-write based on core Gradle classes and concepts
+instead of a port of the Maven Shade code. Documentation for version 0.8 and prior can be found [here](README_old.md).
+
+## Current Status
+
+<a href='https://bintray.com/johnrengelman/gradle-plugins/gradle-shadow-plugin/view?source=watch' alt='Get automatic notifications about new "gradle-shadow-plugin" versions'><img src='https://www.bintray.com/docs/images/bintray_badge_color.png'></a>
+[ ![Download](https://api.bintray.com/packages/johnrengelman/gradle-plugins/gradle-shadow-plugin/images/download.png) ](https://bintray.com/johnrengelman/gradle-plugins/gradle-shadow-plugin/_latestVersion)
+[![Circle CI](https://circleci.com/gh/johnrengelman/shadow.png?style=badge)](https://circleci.com/gh/johnrengelman/shadow)
+
+## Gradle Plugins
+
+https://plugins.gradle.org/plugin/com.github.johnrengelman.shadow
+
+## QuickStart
+
+### Applying Shadow Plugin to Project
+
+#### Gradle 1.x and 2.0
+
+```
+buildscript {
+ repositories { jcenter() }
+ dependencies {
+ classpath 'com.github.jengelman.gradle.plugins:shadow:1.2.2'
+ }
+}
+
+apply plugin: 'java' // or 'groovy'. Must be explicitly applied
+apply plugin: 'com.github.johnrengelman.shadow'
+```
+
+#### Gradle 2.1 and higher
+
+```
+plugins {
+ id 'java' // or 'groovy' Must be explicitly applied
+ id 'com.github.johnrengelman.shadow' version '1.2.2'
+}
+```
+
+Note: Applying the `ShadowPlugin` to a project applies the majority of its settings via a callback on the application of
+other plugins. For example, the bulk of `shadow` is only added to the project if the `java` or `groovy` plugins are also
+added. Shadow will **not** add them automatically, but instead listens for their application and responds.
+
+### Using the default plugin task
+
+```
+$ gradle shadowJar //shadow the runtime configuration with project code into ./build/libs/
+```
+
+`shadowJar` uses the same default configurations as `jar` and additionally configures the `classifier` to be `'all'`.
+Additionally, it creates a `'shadow'` configuration and assigns the jar as an artifact of it. This configuration can
+be used to add dependencies that are excluded from the shadowing.
+
+### Integrating with Application Plugin
+
+```
+apply plugin: 'application'
+apply plugin: 'com.github.johnrengelman.shadow'
+```
+
+Applying both `shadow` and `application` to a project will create a number of additional tasks to be created. These
+tasks mimic the `application` plugin but execute using the output of the `shadowJar` task.
+
+Applying the `application` plugin will cause the `shadowJar` to include the `Main-Class` attribute in the manifest of
+the `shadowJar` output. This is configured via the `mainClassName` attribute from the `application` plugin.
+
+## Advanced Configuration
+
+### Configure MANIFEST file
+
+By default, shadowJar.manifest inherits from jar.manifest.
+
+```
+jar {
+ manifest {
+ attributes("Implementation-Title": "Gradle", "Implementation-Version": version)
+ }
+}
+```
+
+### Modifying the MANIFEST file
+
+Append to the Jar MANIFEST. Values specified here, override the values in jar.manifest.
+
+```
+shadowJar {
+ manifest {
+ attributes 'Test-Entry': 'PASSED'
+ }
+}
+```
+
+### Merging Service files
+
+```
+shadowJar {
+ mergeServiceFiles()
+}
+```
+
+**OR**
+
+```
+import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer
+
+shadowJar {
+ transform(ServiceFileTransformer)
+}
+```
+
+### Merging service files in a different directory
+
+```
+shadowJar {
+ mergeServiceFiles('META-INF/griffon')
+}
+```
+
+**OR**
+
+```
+import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer
+
+shadowJar {
+ transform(ServiceFileTransformer) {
+ path = 'META-INF/griffon'
+ }
+}
+```
+
+### Merging service files specified by include and exclude patterns
+
+```
+shadowJar {
+ mergeServiceFiles {
+ exclude 'META-INF/services/com.acme.*'
+ }
+}
+```
+
+**OR**
+
+```
+import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer
+
+shadowJar {
+ transform(ServiceFileTransformer) {
+ exclude 'META-INF/services/com.acme.*'
+ }
+}
+```
+
+### Merging Groovy extension modules
+
+```
+shadowJar {
+ mergeGroovyExtensionModules()
+}
+```
+
+**OR**
+
+```
+import com.github.jengelman.gradle.plugins.shadow.transformers.GroovyExtensionModuleTransformer
+
+shadowJar {
+ transform(GroovyExtensionModuleTransformer)
+}
+```
+
+
+### Appending Files
+
+```
+shadowJar {
+ append('NOTICE')
+}
+```
+
+**OR**
+
+```
+import com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer
+
+shadowJar {
+ transform(AppendingTransformer) {
+ resource = 'NOTICE'
+ }
+}
+```
+
+### Filtering shadow jar contents by file pattern
+
+```
+shadowJar {
+ exclude 'LICENSE'
+}
+```
+
+### Filtering shadow jar contents by maven/project dependency
+
+Exclude specific dependency (transitive dependencies are **not** excluded).
+
+```
+shadowJar {
+ dependencies {
+ exclude(dependency('asm:asm:3.3.1'))
+ }
+}
+```
+
+Include specific dependency (transitive dependencies are **not** included). Note that dependency inclusion is based
+on the same core classes as Gradle's `CopySpec` inclusion/exclusion. By default, there is a global include, however,
+declaring a specific `include` effectively creates a global `exclude`. That is, once an `include` is made, only items
+that are specifically listed for inclusion will be include in the final output.
+
+```
+shadowJar {
+ dependencies {
+ include(dependency('asm:asm:3.3.1'))
+ }
+}
+```
+
+Include or exclude dependencies using regex pattern matching. The string is split on the `:` character in the form
+`<group>:<name>:<version>`. Each piece is compared as a regex to the values of the resolved dependencies.
+
+```
+shadowJar {
+ dependencies {
+ include(dependency('asm:asm:.*'))
+ }
+}
+```
+
+If a piece of the string is not specified, then that field is not used for the matching. Thus the following syntax
+results in the same filtering as the example above.
+
+```
+shadowJar {
+ dependencies {
+ include(dependency('asm:asm'))
+ }
+}
+```
+
+Exclude a project dependency in a multi-project build.
+
+```
+shadowJar {
+ dependencies {
+ exclude(project(":myclient"))
+ }
+}
+```
+
+### Relocating dependencies
+
+```
+shadowJar {
+ relocate 'org.objectweb.asm', 'myjarjarasm.asm'
+}
+```
+
+### Filtering files in relocation
+
+```
+shadowJar {
+ relocate('org.objectweb.asm', 'myjarjarasm.asm') {
+ exclude 'org.objectweb.asm.ClassReader'
+ }
+}
+```
+
+### Transforming resources
+
+Uses the [Transformer](src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/Transformer.groovy) interface.
+
+```
+shadowJar {
+ transform(<Transformer class>) {
+ //..configure the Transformer class instance
+ }
+}
+```
+
+### Publishing the shadow jar as an additional resource to the main jar
+
+```
+apply plugin: 'com.github.johnrengelman.shadow'
+apply plugin: 'maven-publish'
+
+publishing {
+ publications {
+ shadow(MavenPublication) {
+ from components.java
+ artifact shadowJar
+ }
+ }
+}
+```
+
+### Publishing the shadow jar as a standalone artifact
+
+```
+apply plugin: 'com.github.johnrengelman.shadow'
+apply plugin: 'maven-publish'
+
+shadowJar {
+ baseName = 'myproject-all'
+ classifier = ''
+}
+
+publishing {
+ publications {
+ shadow(MavenPublication) {
+ from components.shadow
+ artifactId = 'myproject-all'
+ }
+ }
+}
+```
+
+OR
+
+```
+apply plugin: 'com.github.johnrengelman.shadow'
+apply plugin: 'maven'
+
+shadowJar {
+ baseName = 'myproject-all'
+ classifier = ''
+}
+
+uploadShadow {
+ repositories {
+ mavenDeployer {
+ //configure maven deployment
+ }
+ }
+}
+```
+
+*_NOTE_*: When using the `'maven'` plugin, the `'compile'` and `'runtime'` configurations are removed from the POM and
+the `'shadow'` configuration is mapped as `'runtime'` scope. This is identical to the behavior with the
+`'maven-publish'` plugin
+
+### Configuring additional POM dependencies for Shadow Jar
+
+```
+dependencies {
+ compile 'asm:asm:3.3.1'
+ compile 'org.bouncycastle:bcprov-jdk15on:1.47'
+ shadow 'org.bouncycastle:bcprov-jdk15on:1.47'
+}
+
+shadowJar {
+ dependencies {
+ exclude(dependency('org.bouncycastle:bcprov-jdk15on:1.47'))
+ }
+}
+```
+
+This examples allows the project to compile against the BouncyCastle encryption library, but then excludes it from
+the shadowed jar, but including it as a dependency on the 'shadow' configuration.
+
+Additionally, any dependencies added to the `shadow` configuration will be added to the `Class-Path` attribute in
+the JAR Manifest for the output of `shadowJar`.
+
+## ChangeLog
+
+[ChangeLog](ChangeLog.md)
diff --git a/README_old.md b/README_old.md
new file mode 100644
index 0000000..647545f
--- /dev/null
+++ b/README_old.md
@@ -0,0 +1,176 @@
+Gradle Shadow
+=============
+
+Shadow is a port of the Maven Shade plugin to the Gradle framework and Groovy. Where possible, the original
+Shade code has been retained except for porting the files from Java to Groovy. Additionally, test cases included
+in the Shade plugin have been retained where possible.
+
+Not all of Shade's features are implemented within Shadow (although the code maybe be ported). Please see the Feature
+Backlog below.
+
+Current Status
+=============
+
+Latest Release: 0.8 (Released 1/3/2014)
+
+[![Build Status](https://drone.io/github.com/johnrengelman/shadow/status.png)](https://drone.io/github.com/johnrengelman/shadow/latest)
+
+How to use
+=============
+
++ Apply the plugin to your Gradle build file
+
+ buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.github.jengelman.gradle.plugins:shadow:0.8'
+ }
+ }
+
+ apply plugin: 'shadow'
+
++ Configure Shadow using the 'shadow' keyword in your Gradle build file. For example, you probably want to exclude
+jar signature files
+
+ shadow {
+ exclude 'META-INF/*.DSA'
+ exclude 'META-INF/*.RSA'
+ }
+
++ Call the Shadow task
+
+ $ gradle shadowJar
+
++ The shadow artifact will be created in your configured build directory (by default: build/distributions/<project>-<version>-shadow.jar
+
+Configuration Options
+=====================
+
++ destinationDir - configures the output directory for shadow. Default: $buildDir/distributions/
++ baseName - configures the base name of the output file. Default: ${archivesBaseName}-${version}-${classifier}
++ classifier - the classifier the append to the artifact. Default: shadow
++ extension - configures the extension of the output file. Default: jar
++ stats - enables/disables output of statistics for Shadow. Useful for analyzing performance. Default: false
++ artifactAttached - if true, keep original jar; else overwrite the default artifact. Default: true
++ groupFilter - configured the inclusion of only specific artifacts to the shadow. Default: * (all artifacts)
++ outputFile - configures a specific file as output for shadow. If set, overrides all naming configurations. Default: not configured
+
+Extensions
+==========
++ Transformers - apply a transformer class to the processing
+
+ import com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer
+ shadow {
+ transformer(AppendingTransformer) {
+ resource = 'META-INF/spring.handlers'
+ }
+ transformer(AppendingTransformer) {
+ resource = 'META-INF/spring.schemas'
+ }
+ }
+
++ Artifact Set - specify the included/excluded artifacts (this includes or excludes specific jars)
+
+ shadow {
+ artifactSet {
+ include 'org.apache.maven.its.shade.aie'
+ exclude '*:b:jar:'
+ }
+ }
+
++ Filters - filter contents of shadow jar by dependency
+
+ shadow {
+ filter('org.apache.maven.its.shade.fac:a') {
+ include '**/a.properties'
+ }
+ filter('org.apache.maven.its.shade.fac:b:client') {
+ exclude 'org/apache/*'
+ exclude 'org/apache/maven/b/'
+ }
+ filter('*:*') {
+ exclude 'org/*'
+ }
+ }
+
+ OR SHORTHAND
+
+ shadow {
+ include 'META-INF/MANIFEST.MF'
+ exclude 'META-INF/*.RSA'
+ }
+
++ Relocators - relocate class from one package to another
+
+ shadow {
+ relocation {
+ pattern = 'junit.textui'
+ shadedPattern = 'a'
+ excludes = ['junit.textui.TestRunner']
+ }
+ }
+
+
+Configuring Output of Signed Libraries
+======================================
+
+It may be useful to not include certain libraries in the shadow jar, but have them available in a know location for
+later packaging. For example, an encryption library must remain signed for the JVM to use it as a security provider.
+Since the signature files are removed by Shadow, it's not worthwile to include it in the Shadow jar. This can be
+accomplished by using the 'signedCompile' and 'signedRuntime' configurations that Shadow provides for these dependencies.
+
+When running Shadow, dependencies declared for these configurations will by copied into a 'signedLibs' folder in the
+configured destination directory and exclude from the collective jar. For example, a project 'foo' has the following:
+
+ dependencies {
+ signedCompile 'org.bouncycastle:bcprov-jdk15on:1.47'
+ }
+
+Results in the following output:
+
+ build
+ libs
+ foo-1.0-shadow.jar
+ signedLibs
+ bcprov-jdk15on-1.47.jar
+
+Good Things To Know
+===================
+
+The default implementation excludes all META-INF/INDEX.LIST files.
+
+Version History
+===============
+
++ v0.8
+ + Changed Maven Group ID to com.github.jengelman.gradle.plugins
+ + Published artifact to JCenter
+ + Upgraded to Gradle 1.10
+ + Main task renamed to be 'shadowJar' instead of 'shadow'. This was done so the task and extension namespace
+ did not collide.
+ + Changed default output location to be ${buildDir}/distributions instead of ${buildDir}/libs
+ + Added support for class Relocation, thanks to [Baron Roberts](https://github.com/baron1405)
++ v0.7.4 - upgrade to Gradle 1.6 internally and remove use of deprecated methods.
++ v0.7.3 - fix bad method call in the AppendingTransformer
++ v0.7.2 - fix a bug that was preventing multiple includes/excludes in the artifactSet. Fix bug in filtering
+shorthand style that caused filters to not be applied.
++ v0.7.1 - fix the up-to-date bug where the shadow task wasn't executing after making a source change. Changed the
+BinTray repo to Maven compatabile instead of Ivy.
++ v0.7 - all the v0.6 features, but using a port of the Shade code. Primarily this involves using a port
+of the DefaultShader class instead of the from scratch implementation used in v0.6. This will allow for integration of
+more of Shade's features with minor changes.
+ + Includes support for SimpleFilter
+ + Includes support for Transformers: ApacheLicenseResourceTransformer, ApacheNoticeResourceTransformer,
+ AppendingTransformer, ComponentsXmlResourceTransformer, DontIncludeResourceTransformer, IncludeResourceTransformer,
+ ManifestResourceTransformer, ServiceFileTransformer, XmlAppendingTransfomer
++ v0.6 - first release, mostly written from scratch using Shade code as reference.
++ v0.5 and earlier - incremental internal releases.
+
+Feature Backlog
+===============
++ Port support for configuration of a custom Caster (Shader) implementation
++ Automatically configure Shadow output as publish artifact
++ Port support for generation of shadow sources jar
++ Port support for minijar filter
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..01f6ae2
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,89 @@
+import org.gradle.wrapper.Download
+import org.gradle.wrapper.GradleUserHomeLookup
+import org.gradle.wrapper.Install
+import org.gradle.wrapper.PathAssembler
+import org.gradle.wrapper.WrapperConfiguration
+import org.gradle.wrapper.WrapperExecutor
+import org.gradle.wrapper.Logger
+
+buildscript {
+ repositories {
+ jcenter()
+ maven {
+ url "https://plugins.gradle.org/m2/"
+ }
+ }
+ dependencies {
+ classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.5'
+ classpath "org.jfrog.buildinfo:build-info-extractor-gradle:3.0.0"
+ classpath "com.gradle.publish:plugin-publish-plugin:0.9.1"
+ classpath "org.asciidoctor:asciidoctor-gradle-plugin:1.5.2"
+ classpath "com.bluepapa32:gradle-watch-plugin:0.1.5"
+ classpath "org.kordamp.gradle:livereload-gradle-plugin:0.2.1"
+ }
+}
+
+apply plugin: 'groovy'
+apply plugin: 'idea'
+apply plugin: 'project-report'
+
+apply from: file('gradle/docs.gradle')
+apply from: file('gradle/publish.gradle')
+
+repositories {
+ mavenLocal()
+ jcenter()
+}
+
+dependencies {
+
+ compile localGroovy()
+ compile gradleApi()
+ compile 'org.jdom:jdom2:2.0.5'
+ compile 'org.ow2.asm:asm:5.0.3'
+ compile 'org.ow2.asm:asm-commons:5.0.3'
+ compile 'commons-io:commons-io:2.4'
+ compile 'org.apache.ant:ant:1.9.4'
+ compile 'org.codehaus.plexus:plexus-utils:2.0.6'
+ compile 'org.codehaus.groovy:groovy-backports-compat23:2.4.4'
+
+
+ testCompile gradleTestKit()
+ testCompile("org.spockframework:spock-core:1.0-groovy-2.4") {
+ exclude module: 'groovy-all'
+ }
+ testCompile 'xmlunit:xmlunit:1.3'
+}
+
+test {
+ dependsOn publishToMavenLocal
+ if (System.env.CI == 'true') {
+ testLogging.showStandardStreams = true
+ }
+}
+
+jar {
+ from rootProject.file('LICENSE')
+ from rootProject.file('NOTICE')
+}
+
+idea {
+ project {
+ jdkName = '1.7'
+ languageLevel = '1.6'
+ }
+}
+
+sourceCompatibility = '1.6'
+targetCompatibility = '1.6'
+
+task installWrappers << {
+ WrapperExecutor wrapperExecutor = WrapperExecutor.forWrapperPropertiesFile(file('gradle/wrapper/gradle-wrapper.properties'), System.out)
+ ['1.12', '2.0', '2.5', '2.11-rc-1'].each {
+ WrapperConfiguration config = wrapperExecutor.config
+ config.setDistribution(new URI("https://services.gradle.org/distributions/gradle-$it-bin.zip"))
+ Install install = new Install(new Logger(true), new Download(new Logger(true), "gradlew", it),
+ new PathAssembler(GradleUserHomeLookup.gradleUserHome()))
+ install.createDist(config)
+ }
+}
diff --git a/circle.yml b/circle.yml
new file mode 100644
index 0000000..68b501f
--- /dev/null
+++ b/circle.yml
@@ -0,0 +1,12 @@
+machine:
+ environment:
+ TERM: dumb
+ GRADLE_OPTS: -Xmx256m
+
+dependencies:
+ override:
+ - ./gradlew installWrappers dependencies
+
+test:
+ override:
+ - ./gradlew check
\ No newline at end of file
diff --git a/gradle/docs.gradle b/gradle/docs.gradle
new file mode 100644
index 0000000..85c1e08
--- /dev/null
+++ b/gradle/docs.gradle
@@ -0,0 +1,54 @@
+def javaApiUrl = 'http://docs.oracle.com/javase/1.7.0/docs/api'
+def groovyApiUrl = 'http://groovy.codehaus.org/gapi/'
+
+apply plugin: 'org.asciidoctor.convert'
+apply plugin: "com.bluepapa32.watch"
+apply plugin: "org.kordamp.gradle.livereload"
+
+tasks.withType(Javadoc) {
+ options.links(javaApiUrl, groovyApiUrl)
+ options.addStringOption('Xdoclint:none', '-quiet')
+}
+
+task javadocJar(type: Jar, dependsOn: javadoc) {
+ classifier = 'javadoc'
+ from 'build/docs/javadoc'
+}
+
+task sourcesJar(type: Jar) {
+ classifier = 'sources'
+ from sourceSets.main.allSource
+}
+
+build.dependsOn javadocJar, sourcesJar
+
+asciidoctor {
+ def source = project.sourceSets.main.java.srcDirs[0]
+
+ // add extra inputs since these include files that are included
+ inputs.dir source
+
+ attributes 'build-gradle': file('build.gradle'),
+ 'sourcedir': source,
+ 'endpoint-url': 'http://example.org',
+
+ 'source-highlighter' : 'coderay',
+ 'imagesdir':'images',
+ 'toc':'left',
+ 'icons': 'font',
+ 'setanchors':'true',
+ 'idprefix':'',
+ 'idseparator':'-',
+ 'docinfo1':'true'
+}
+
+watch {
+ asciidoc {
+ files fileTree('src')
+ tasks 'asciidoctor'
+ }
+}
+
+liveReload {
+ docRoot asciidoctor.outputDir.canonicalPath
+}
\ No newline at end of file
diff --git a/gradle/publish.gradle b/gradle/publish.gradle
new file mode 100644
index 0000000..2080563
--- /dev/null
+++ b/gradle/publish.gradle
@@ -0,0 +1,141 @@
+apply plugin: 'com.jfrog.bintray'
+apply plugin: 'maven-publish'
+apply plugin: 'com.jfrog.artifactory-publish'
+apply plugin: "com.gradle.plugin-publish"
+
+group = 'com.github.jengelman.gradle.plugins'
+def versionString = file('src/main/resources/shadow-version.txt').text.trim()
+version = versionString
+
+ext.isSnapshot = version.endsWith("SNAPSHOT")
+
+def pomConfig = {
+ licenses {
+ license {
+ name 'The Apache Software License, Version 2.0'
+ url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
+ distribution 'repo'
+ }
+ }
+ developers {
+ developer {
+ id 'jengelman'
+ name 'John Engelman'
+ email 'john.r.engelman at gmail.com'
+ }
+ }
+}
+
+publishing {
+ publications {
+ plugin(MavenPublication) {
+ from components.java
+ artifact sourcesJar
+ artifact javadocJar
+
+ pom.withXml {
+ def root = asNode()
+ root.appendNode('description', 'Gradle plugin to combine dependencies into a single Jar. Port of Maven Shade.')
+ root.children().last() + pomConfig
+ }
+ }
+ }
+}
+
+artifactory {
+ contextUrl = 'https://oss.jfrog.org/artifactory'
+ publish {
+ repository {
+ repoKey = 'oss-snapshot-local'
+ }
+ defaults {
+ publications 'plugin'
+ }
+ }
+}
+
+artifactoryPublish { task ->
+ doFirst {
+ if (!isSnapshot) {
+ throw new GradleException('Cannot publish non-SNAPSHOT versions to OJO!')
+ }
+ }
+ gradle.taskGraph.whenReady { taskGraph ->
+ if (taskGraph.hasTask(task)) {
+ project.artifactory {
+ publish {
+ repository {
+ username = bintrayUser
+ password = bintrayKey
+ }
+ }
+ }
+ }
+ }
+}
+
+bintrayUpload { task ->
+ doFirst {
+ if (isSnapshot) {
+ throw new GradleException('Cannot publish SNAPSHOT versions to BinTray!')
+ }
+ }
+ gradle.taskGraph.whenReady { taskGraph ->
+ if (taskGraph.hasTask(task)) {
+ task.user = bintrayUser
+ task.apiKey = bintrayKey
+ }
+ }
+}
+
+bintray {
+ publications = ['plugin']
+ pkg {
+ repo = 'gradle-plugins'
+ name = 'gradle-shadow-plugin'
+ licenses = ['Apache-2.0']
+ desc = 'Create uber-jar containing application code and dependencies.'
+ labels = ['gradle', 'onejar', 'fatjar', 'uberjar', 'shade']
+ websiteUrl = 'https://github.com/johnrengelman/shadow'
+ issueTrackerUrl = 'https://github.com/johnrengelman/shadow/issues'
+ vcsUrl = 'https://github.com/johnrengelman/shadow.git'
+ version {
+ vcsTag = versionString
+ attributes = [
+ 'gradle-plugin': 'com.github.johnrengelman.shadow:com.github.jengelman.gradle.plugins:shadow'
+ ]
+ }
+ }
+}
+
+pluginBundle {
+ website = 'https://github.com/johnrengelman/shadow'
+ vcsUrl = 'https://github.com/johnrengelman/shadow'
+ description = 'A Gradle plugin for collapsing all dependencies and project code into a single Jar file.'
+ tags = ['onejar', 'shade', 'fatjar', 'uberjar']
+
+ plugins {
+ shadowPlugin {
+ id = 'com.github.johnrengelman.shadow'
+ displayName = 'Shadow Plugin'
+ }
+ }
+
+ mavenCoordinates {
+ groupId = project.group
+ artifactId = project.name
+
+ }
+}
+
+publishPlugins { task ->
+ doFirst {
+ if (isSnapshot) {
+ throw new GradleException('Cannot publish SNAPSHOT versions to Plugin Portal!')
+ }
+ }
+}
+
+task release() {
+ dependsOn 'assemble', 'bintrayUpload', 'publishPlugins'
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..13372ae
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..53b936d
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jan 22 08:36:09 CST 2016
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-bin.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..9d82f78
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..aec9973
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+ at if "%DEBUG%" == "" @echo off
+ at rem ##########################################################################
+ at rem
+ at rem Gradle startup script for Windows
+ at rem
+ at rem ##########################################################################
+
+ at rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+ at rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+ at rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+ at rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+ at rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+ at rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+ at rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+ at rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+ at rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..1e25357
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'shadow'
diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc
new file mode 100644
index 0000000..76ab2b3
--- /dev/null
+++ b/src/docs/asciidoc/index.adoc
@@ -0,0 +1,81 @@
+= Shadow Plugin User Guide & Examples
+John Engelman - @johnrengelman
+:revnumber: {project-version}
+:example-caption!:
+ifndef::imagesdir[:imagesdir: images]
+ifndef::sourcedir[:sourcedir: ../groovy]
+
+This is a user manual for an example project.
+
+== Introduction
+
+This project does something.
+We just haven't decided what that is yet.
+
+== Source Code
+
+[source,java]
+.Java code from project
+----
+include::{sourcedir}/example/StringUtils.java[tags=contains,indent=0]
+----
+
+This page was built by the following command:
+
+ $ ./gradlew asciidoctor
+
+== Live Reload Support
+
+The Gradle build also can support http://asciidoctor.org/docs/editing-asciidoc-with-live-preview/#livereload[Live Reload]
+
+Start by running the build with the watch task
+
+ $ ./gradlew asciidoctor watch
+
+This ensures if you modify example-manual.adoc then everything is rebuilt.
+
+Open another terminal and run the liveReload task:
+
+ $ ./gradlew liveReload
+
+Now you can use the https://chrome.google.com/webstore/detail/livereload/jnihajbhpnppcggbcgedagnkighmdlei?hl=en[Chrome] or http://feedback.livereload.com/knowledgebase/articles/86242-how-do-i-install-and-use-the-browser-extensions-[Firefox] plugins to automatically reload your browser when the build completes.
+
+IMPORTANT: After installing the Chrome LiveReload extension, you need to check the "Allow access to file URLs" checkbox in Tools > Extensions > LiveReload in order for it to work with local files.
+
+Now try editing example-manual.adoc and watching your browser.
+After a brief wait you will observe that the browser is automatically updated with your changes.
+
+== Images
+
+[.thumb]
+image::sunset.jpg[scaledwidth=75%]
+
+== Attributes
+
+.Built-in
+asciidoctor-version:: {asciidoctor-version}
+safe-mode-name:: {safe-mode-name}
+docdir:: {docdir}
+docfile:: {docfile}
+imagesdir:: {imagesdir}
+
+.Custom
+project-version:: {project-version}
+sourcedir:: {sourcedir}
+endpoint-url:: {endpoint-url}
+
+== Includes
+
+.include::subdir/_b.adoc[]
+====
+include::subdir/_b.adoc[]
+====
+
+WARNING: Includes can be tricky!
+
+== build.gradle
+
+[source,groovy]
+----
+include::{build-gradle}[]
+----
\ No newline at end of file
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.groovy
new file mode 100644
index 0000000..d633642
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.groovy
@@ -0,0 +1,149 @@
+package com.github.jengelman.gradle.plugins.shadow
+
+import com.github.jengelman.gradle.plugins.shadow.internal.JavaJarExec
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowCreateStartScripts
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
+import org.gradle.api.GradleException
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.file.CopySpec
+import org.gradle.api.plugins.ApplicationPlugin
+import org.gradle.api.plugins.ApplicationPluginConvention
+import org.gradle.api.tasks.Sync
+import org.gradle.api.tasks.bundling.AbstractArchiveTask
+import org.gradle.api.tasks.bundling.Tar
+import org.gradle.api.tasks.bundling.Zip
+
+class ShadowApplicationPlugin implements Plugin<Project> {
+
+ static final String SHADOW_RUN_TASK_NAME = 'runShadow'
+ static final String SHADOW_SCRIPTS_TASK_NAME = 'startShadowScripts'
+ static final String SHADOW_INSTALL_TASK_NAME = 'installShadowApp'
+ static final String SHADOW_ZIP_DIST_TASK_NAME = 'distShadowZip'
+ static final String SHADOW_TAR_DIST_TASK_NAME = 'distShadowTar'
+
+ private Project project
+
+ @Override
+ void apply(Project project) {
+ this.project = project
+
+ addRunTask(project)
+ addCreateScriptsTask(project)
+
+ ShadowExtension extension = project.extensions.findByType(ShadowExtension)
+ configureDistSpec(project, extension.applicationDistribution)
+
+ configureJarMainClass(project)
+ addInstallTask(project)
+ addDistZipTask(project)
+ addDistTarTask(project)
+ }
+
+ protected void configureJarMainClass(Project project) {
+ ApplicationPluginConvention pluginConvention = (
+ ApplicationPluginConvention) project.convention.plugins.application
+
+ jar.doFirst {
+ manifest.attributes 'Main-Class': pluginConvention.mainClassName
+ }
+ }
+
+ protected void addRunTask(Project project) {
+ ApplicationPluginConvention pluginConvention = (
+ ApplicationPluginConvention) project.convention.plugins.application
+
+ def run = project.tasks.create(SHADOW_RUN_TASK_NAME, JavaJarExec)
+ run.dependsOn SHADOW_INSTALL_TASK_NAME
+ run.description = 'Runs this project as a JVM application using the shadow jar'
+ run.group = ApplicationPlugin.APPLICATION_GROUP
+ run.conventionMapping.jvmArgs = { pluginConvention.applicationDefaultJvmArgs }
+ run.conventionMapping.jarFile = {
+ project.file("${project.buildDir}/installShadow/${pluginConvention.applicationName}/lib/${jar.archivePath.name}")
+ }
+ }
+
+ protected void addCreateScriptsTask(Project project) {
+ ApplicationPluginConvention pluginConvention =
+ (ApplicationPluginConvention) project.convention.plugins.application
+
+ def startScripts = project.tasks.create(SHADOW_SCRIPTS_TASK_NAME, ShadowCreateStartScripts)
+ startScripts.description = 'Creates OS specific scripts to run the project as a JVM application using the shadow jar'
+ startScripts.group = ApplicationPlugin.APPLICATION_GROUP
+ startScripts.conventionMapping.mainApplicationJar = { jar.archivePath }
+ startScripts.conventionMapping.applicationName = { pluginConvention.applicationName }
+ startScripts.conventionMapping.outputDir = { new File(project.buildDir, 'scriptsShadow') }
+ startScripts.conventionMapping.defaultJvmOpts = { pluginConvention.applicationDefaultJvmArgs }
+ startScripts.inputs.file jar
+ }
+
+ protected void addInstallTask(Project project) {
+ ApplicationPluginConvention pluginConvention =
+ (ApplicationPluginConvention) project.convention.plugins.application
+ ShadowExtension extension = project.extensions.findByType(ShadowExtension)
+
+ def installTask = project.tasks.create(SHADOW_INSTALL_TASK_NAME, Sync)
+ installTask.description = "Installs the project as a JVM application along with libs and OS specific scripts."
+ installTask.group = ApplicationPlugin.APPLICATION_GROUP
+ installTask.with extension.applicationDistribution
+ installTask.into { project.file("${project.buildDir}/installShadow/${pluginConvention.applicationName}") }
+ installTask.doFirst {
+ if (destinationDir.directory) {
+ if (!new File(destinationDir, 'lib').directory || !new File(destinationDir, 'bin').directory) {
+ throw new GradleException("The specified installation directory '${destinationDir}' is neither empty nor does it contain an installation for '${pluginConvention.applicationName}'.\n" +
+ "If you really want to install to this directory, delete it and run the install task again.\n" +
+ "Alternatively, choose a different installation directory."
+ )
+ }
+ }
+ }
+ installTask.doLast {
+ project.ant.chmod(file: "${destinationDir.absolutePath}/bin/${pluginConvention.applicationName}", perm: 'ugo+x')
+ }
+ }
+
+ protected void addDistZipTask(Project project) {
+ addArchiveTask(project, SHADOW_ZIP_DIST_TASK_NAME, Zip)
+ }
+
+ protected void addDistTarTask(Project project) {
+ addArchiveTask(project, SHADOW_TAR_DIST_TASK_NAME, Tar)
+ }
+
+ protected <T extends AbstractArchiveTask> void addArchiveTask(Project project, String name, Class<T> type) {
+ ApplicationPluginConvention pluginConvention = project.convention.plugins.application
+ ShadowExtension extension = project.extensions.findByType(ShadowExtension)
+
+ def archiveTask = project.tasks.create(name, type)
+ archiveTask.description = "Bundles the project as a JVM application with libs and OS specific scripts."
+ archiveTask.group = ApplicationPlugin.APPLICATION_GROUP
+ archiveTask.conventionMapping.baseName = { pluginConvention.applicationName }
+ def baseDir = { archiveTask.archiveName - ".${archiveTask.extension}" }
+ archiveTask.into(baseDir) {
+ with(extension.applicationDistribution)
+ }
+ }
+
+ protected CopySpec configureDistSpec(Project project, CopySpec distSpec) {
+ def startScripts = project.tasks.startShadowScripts
+
+ distSpec.with {
+ from(project.file("src/dist"))
+
+ into("lib") {
+ from(jar)
+ from(project.configurations.shadow)
+ }
+ into("bin") {
+ from(startScripts)
+ fileMode = 0755
+ }
+ }
+
+ distSpec
+ }
+
+ private ShadowJar getJar() {
+ project.tasks.findByName(ShadowJavaPlugin.SHADOW_JAR_TASK_NAME)
+ }
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowBasePlugin.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowBasePlugin.groovy
new file mode 100644
index 0000000..ff3da44
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowBasePlugin.groovy
@@ -0,0 +1,25 @@
+package com.github.jengelman.gradle.plugins.shadow
+
+import com.github.jengelman.gradle.plugins.shadow.tasks.KnowsTask
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+
+class ShadowBasePlugin implements Plugin<Project> {
+
+ static final String EXTENSION_NAME = 'shadow'
+ static final String CONFIGURATION_NAME = 'shadow'
+
+ @Override
+ void apply(Project project) {
+ project.extensions.create(EXTENSION_NAME, ShadowExtension, project)
+ createShadowConfiguration(project)
+
+ KnowsTask knows = project.tasks.create(KnowsTask.NAME, KnowsTask)
+ knows.group = ShadowJavaPlugin.SHADOW_GROUP
+ knows.description = KnowsTask.DESC
+ }
+
+ private void createShadowConfiguration(Project project) {
+ project.configurations.create(CONFIGURATION_NAME)
+ }
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowExtension.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowExtension.groovy
new file mode 100644
index 0000000..93c9191
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowExtension.groovy
@@ -0,0 +1,13 @@
+package com.github.jengelman.gradle.plugins.shadow
+
+import org.gradle.api.Project
+import org.gradle.api.file.CopySpec
+
+class ShadowExtension {
+
+ CopySpec applicationDistribution
+
+ ShadowExtension(Project project) {
+ applicationDistribution = project.copySpec {}
+ }
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.groovy
new file mode 100644
index 0000000..c8a2b14
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.groovy
@@ -0,0 +1,93 @@
+package com.github.jengelman.gradle.plugins.shadow
+
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
+import org.gradle.api.Action
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.artifacts.DependencySet
+import org.gradle.api.artifacts.PublishArtifact
+import org.gradle.api.artifacts.maven.Conf2ScopeMappingContainer
+import org.gradle.api.artifacts.maven.MavenPom
+import org.gradle.api.internal.java.JavaLibrary
+import org.gradle.api.plugins.JavaPluginConvention
+import org.gradle.api.plugins.MavenPlugin
+import org.gradle.api.tasks.Upload
+import org.gradle.configuration.project.ProjectConfigurationActionContainer
+
+import javax.inject.Inject
+
+class ShadowJavaPlugin implements Plugin<Project> {
+
+ static final String SHADOW_JAR_TASK_NAME = 'shadowJar'
+ static final String SHADOW_UPLOAD_TASK = 'uploadShadow'
+ static final String SHADOW_COMPONENT_NAME = 'shadow'
+ static final String SHADOW_GROUP = 'Shadow'
+
+ private final ProjectConfigurationActionContainer configurationActionContainer;
+
+ @Inject
+ ShadowJavaPlugin(ProjectConfigurationActionContainer configurationActionContainer) {
+ this.configurationActionContainer = configurationActionContainer
+ }
+
+ @Override
+ void apply(Project project) {
+ configureShadowTask(project)
+ }
+
+ protected void configureShadowTask(Project project) {
+ JavaPluginConvention convention = project.convention.getPlugin(JavaPluginConvention)
+ ShadowJar shadow = project.tasks.create(SHADOW_JAR_TASK_NAME, ShadowJar)
+ shadow.group = SHADOW_GROUP
+ shadow.description = 'Create a combined JAR of project and runtime dependencies'
+ shadow.conventionMapping.with {
+ map('classifier') {
+ 'all'
+ }
+ }
+ shadow.manifest.inheritFrom project.tasks.jar.manifest
+ shadow.doFirst {
+ def files = project.configurations.findByName(ShadowBasePlugin.CONFIGURATION_NAME).files
+ if (files) {
+ def libs = [project.tasks.jar.manifest.attributes.get('Class-Path')]
+ libs.addAll files.collect { "${it.name}" }
+ manifest.attributes 'Class-Path': libs.findAll { it }.join(' ')
+ }
+ }
+ shadow.from(convention.sourceSets.main.output)
+ shadow.configurations = [project.configurations.runtime]
+ shadow.exclude('META-INF/INDEX.LIST', 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA')
+
+ PublishArtifact shadowArtifact = project.artifacts.add(ShadowBasePlugin.CONFIGURATION_NAME, shadow)
+ project.components.add(new ShadowJavaLibrary(shadowArtifact, project.configurations.shadow.allDependencies))
+ configureShadowUpload()
+ }
+
+ private void configureShadowUpload() {
+ configurationActionContainer.add(new Action<Project>() {
+ public void execute(Project project) {
+ Upload upload = project.tasks.withType(Upload).findByName(SHADOW_UPLOAD_TASK)
+ if (!upload) {
+ return
+ }
+ upload.configuration = project.configurations.shadow
+ MavenPom pom = upload.repositories.mavenDeployer.pom
+ pom.scopeMappings.mappings.remove(project.configurations.compile)
+ pom.scopeMappings.mappings.remove(project.configurations.runtime)
+ pom.scopeMappings.addMapping(MavenPlugin.RUNTIME_PRIORITY, project.configurations.shadow, Conf2ScopeMappingContainer.RUNTIME)
+ }
+ })
+ }
+
+ class ShadowJavaLibrary extends JavaLibrary {
+
+ ShadowJavaLibrary(PublishArtifact jarArtifact, DependencySet runtimeDependencies) {
+ super(jarArtifact, runtimeDependencies)
+ }
+
+ @Override
+ String getName() {
+ return SHADOW_COMPONENT_NAME
+ }
+ }
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPlugin.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPlugin.groovy
new file mode 100644
index 0000000..d414268
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPlugin.groovy
@@ -0,0 +1,20 @@
+package com.github.jengelman.gradle.plugins.shadow
+
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.plugins.ApplicationPlugin
+import org.gradle.api.plugins.JavaPlugin
+
+class ShadowPlugin implements Plugin<Project> {
+
+ @Override
+ void apply(Project project) {
+ project.plugins.apply(ShadowBasePlugin)
+ project.plugins.withType(JavaPlugin) {
+ project.plugins.apply(ShadowJavaPlugin)
+ }
+ project.plugins.withType(ApplicationPlugin) {
+ project.plugins.apply(ShadowApplicationPlugin)
+ }
+ }
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowStats.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowStats.groovy
new file mode 100644
index 0000000..8009b2b
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowStats.groovy
@@ -0,0 +1,60 @@
+package com.github.jengelman.gradle.plugins.shadow
+
+import groovy.util.logging.Slf4j
+import org.gradle.api.GradleException
+
+ at Slf4j
+class ShadowStats {
+
+ long totalTime
+ long jarStartTime
+ long jarEndTime
+ int jarCount = 1
+ boolean processingJar
+
+ void startJar() {
+ if (processingJar) throw new GradleException("Can only time one entry at a time")
+ processingJar = true
+ jarStartTime = System.currentTimeMillis()
+ }
+
+ void finishJar() {
+ if (processingJar) {
+ jarEndTime = System.currentTimeMillis()
+ jarCount++
+ totalTime += jarTiming
+ processingJar = false
+ }
+ }
+
+ void printStats() {
+ println this
+ }
+
+ long getJarTiming() {
+ jarEndTime - jarStartTime
+ }
+
+ double getTotalTimeSecs() {
+ totalTime / 1000
+ }
+
+ double getAverageTimePerJar() {
+ totalTime / jarCount
+ }
+
+ double getAverageTimeSecsPerJar() {
+ averageTimePerJar / 1000
+ }
+
+ String toString() {
+ StringBuilder sb = new StringBuilder()
+ sb.append "*******************\n"
+ sb.append "GRADLE SHADOW STATS\n"
+ sb.append "\n"
+ sb.append "Total Jars: $jarCount (includes project)\n"
+ sb.append "Total Time: ${totalTimeSecs}s [${totalTime}ms]\n"
+ sb.append "Average Time/Jar: ${averageTimeSecsPerJar}s [${averageTimePerJar}ms]\n"
+ sb.append "*******************"
+ }
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/impl/RelocatorRemapper.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/impl/RelocatorRemapper.groovy
new file mode 100644
index 0000000..482fae8
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/impl/RelocatorRemapper.groovy
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.impl
+
+import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowCopyAction.RelativeArchivePath
+import org.objectweb.asm.commons.Remapper
+
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+/**
+ * Modified from org.apache.maven.plugins.shade.DefaultShader.java#RelocatorRemapper
+ *
+ * Modifications
+ * @author John Engelman
+ */
+class RelocatorRemapper extends Remapper {
+
+ private final Pattern classPattern = Pattern.compile("(\\[*)?L(.+)")
+
+ List<Relocator> relocators
+
+ RelocatorRemapper(List<Relocator> relocators) {
+ this.relocators = relocators
+ }
+
+ boolean hasRelocators() {
+ return !relocators.empty
+ }
+
+ Object mapValue(Object object) {
+ if (object instanceof String) {
+ String name = (String) object
+ String value = name
+
+ String prefix = ""
+ String suffix = ""
+
+ Matcher m = classPattern.matcher(name)
+ if (m.matches()) {
+ prefix = m.group(1) + "L"
+ suffix = ""
+ name = m.group(2)
+ }
+
+ for (Relocator r : relocators) {
+ if (r.canRelocateClass(name)) {
+ value = prefix + r.relocateClass(name) + suffix
+ break
+ } else if (r.canRelocatePath(name)) {
+ value = prefix + r.relocatePath(name) + suffix
+ break
+ }
+ }
+
+ return value
+ }
+
+ return super.mapValue(object)
+ }
+
+ String map(String name) {
+ String value = name
+
+ String prefix = ""
+ String suffix = ""
+
+ Matcher m = classPattern.matcher(name)
+ if (m.matches()) {
+ prefix = m.group(1) + "L"
+ suffix = ""
+ name = m.group(2)
+ }
+
+ for (Relocator r : relocators) {
+ if (r.canRelocatePath(name)) {
+ value = prefix + r.relocatePath(name) + suffix
+ break
+ }
+ }
+
+ return value
+ }
+
+ String mapPath(String path) {
+ map(path.substring(0, path.indexOf('.')))
+ }
+
+ String mapPath(RelativeArchivePath path) {
+ mapPath(path.pathString)
+ }
+
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.groovy
new file mode 100644
index 0000000..3cb7dee
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.groovy
@@ -0,0 +1,128 @@
+package com.github.jengelman.gradle.plugins.shadow.internal
+
+import groovy.util.logging.Slf4j
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.artifacts.Dependency
+import org.gradle.api.artifacts.ResolvedDependency
+import org.gradle.api.file.FileCollection
+import org.gradle.api.specs.Spec
+import org.gradle.api.specs.Specs
+
+ at Slf4j
+class DefaultDependencyFilter implements DependencyFilter {
+
+ private final Project project
+
+ private final List<Spec<? super ResolvedDependency>> includeSpecs = []
+ private final List<Spec<? super ResolvedDependency>> excludeSpecs = []
+
+ DefaultDependencyFilter(Project project) {
+ assert project
+ this.project = project
+ }
+
+ FileCollection resolve(Configuration configuration) {
+ Set<ResolvedDependency> includedDeps = []
+ Set<ResolvedDependency> excludedDeps = []
+ resolve(configuration.resolvedConfiguration.firstLevelModuleDependencies, includedDeps, excludedDeps)
+ return project.files(configuration.files) - project.files(excludedDeps.collect {
+ it.moduleArtifacts*.file
+ }.flatten())
+ }
+
+ FileCollection resolve(Collection<Configuration> configurations) {
+ configurations.collect {
+ resolve(it)
+ }.sum() as FileCollection ?: project.files()
+ }
+
+ /**
+ * Exclude dependencies that match the provided spec.
+ *
+ * @param spec
+ * @return
+ */
+ DependencyFilter exclude(Spec<? super ResolvedDependency> spec) {
+ excludeSpecs << spec
+ return this
+ }
+
+ /**
+ * Include dependencies that match the provided spec.
+ *
+ * @param spec
+ * @return
+ */
+ DependencyFilter include(Spec<? super ResolvedDependency> spec) {
+ includeSpecs << spec
+ return this
+ }
+
+ /**
+ * Create a spec that matches the provided project notation on group, name, and version
+ * @param notation
+ * @return
+ */
+ Spec<? super ResolvedDependency> project(Map<String, ?> notation) {
+ dependency(project.dependencies.project(notation))
+ }
+
+ /**
+ * Create a spec that matches the default configuration for the provided project path on group, name, and version
+ *
+ * @param notation
+ * @return
+ */
+ Spec<? super ResolvedDependency> project(String notation) {
+ dependency(project.dependencies.project(path: notation, configuration: 'default'))
+ }
+
+ /**
+ * Create a spec that matches dependencies using the provided notation on group, name, and version
+ * @param notation
+ * @return
+ */
+ Spec<? super ResolvedDependency> dependency(Object notation) {
+ dependency(project.dependencies.create(notation))
+ }
+
+ /**
+ * Create a spec that matches the provided dependency on group, name, and version
+ * @param dependency
+ * @return
+ */
+ Spec<? super ResolvedDependency> dependency(Dependency dependency) {
+ this.dependency({ ResolvedDependency it ->
+ (!dependency.group || it.moduleGroup.matches(dependency.group)) &&
+ (!dependency.name || it.moduleName.matches(dependency.name)) &&
+ (!dependency.version || it.moduleVersion.matches(dependency.version))
+ })
+ }
+
+ /**
+ * Create a spec that matches the provided closure
+ * @param spec
+ * @return
+ */
+ Spec<? super ResolvedDependency> dependency(Closure spec) {
+ return Specs.<ResolvedDependency>convertClosureToSpec(spec)
+ }
+
+
+ protected void resolve(Set<ResolvedDependency> dependencies,
+ Set<ResolvedDependency> includedDependencies,
+ Set<ResolvedDependency> excludedDependencies) {
+ dependencies.each {
+ if (isIncluded(it) ? includedDependencies.add(it) : excludedDependencies.add(it)) {
+ resolve(it.children, includedDependencies, excludedDependencies)
+ }
+ }
+ }
+
+ protected boolean isIncluded(ResolvedDependency dependency) {
+ boolean include = includeSpecs.empty || includeSpecs.any { it.isSatisfiedBy(dependency) }
+ boolean exclude = !excludeSpecs.empty && excludeSpecs.any { it.isSatisfiedBy(dependency) }
+ return include && !exclude
+ }
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/DefaultZipCompressor.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/DefaultZipCompressor.groovy
new file mode 100644
index 0000000..ba8ce4c
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/DefaultZipCompressor.groovy
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+package com.github.jengelman.gradle.plugins.shadow.internal
+
+import org.apache.tools.zip.Zip64Mode
+import org.apache.tools.zip.ZipOutputStream
+import org.gradle.api.UncheckedIOException
+
+
+public class DefaultZipCompressor implements ZipCompressor {
+ private final int entryCompressionMethod;
+ private final Zip64Mode zip64Mode;
+
+ public DefaultZipCompressor(boolean allowZip64Mode, int entryCompressionMethod) {
+ this.entryCompressionMethod = entryCompressionMethod;
+ zip64Mode = allowZip64Mode ? Zip64Mode.AsNeeded : Zip64Mode.Never;
+ }
+
+ public ZipOutputStream createArchiveOutputStream(File destination) {
+ try {
+ OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(destination))
+ ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream);
+ zipOutputStream.setUseZip64(zip64Mode);
+ zipOutputStream.setMethod(entryCompressionMethod);
+ return zipOutputStream;
+ } catch (Exception e) {
+ String message = String.format("Unable to create ZIP output stream for file %s.", destination);
+ throw new UncheckedIOException(message, e);
+ }
+ }
+
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/DependencyFilter.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/DependencyFilter.groovy
new file mode 100644
index 0000000..b2c072c
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/DependencyFilter.groovy
@@ -0,0 +1,76 @@
+package com.github.jengelman.gradle.plugins.shadow.internal
+
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.artifacts.Dependency
+import org.gradle.api.artifacts.ResolvedDependency
+import org.gradle.api.file.FileCollection
+import org.gradle.api.specs.Spec
+
+interface DependencyFilter {
+
+ /**
+ * Resolve a Configuration against the include/exclude rules in the filter
+ * @param configuration
+ * @return
+ */
+ FileCollection resolve(Configuration configuration)
+
+ /**
+ * Resolve all Configurations against the include/exclude ruels in the filter and combine the results
+ * @param configurations
+ * @return
+ */
+ FileCollection resolve(Collection<Configuration> configurations)
+
+ /**
+ * Exclude dependencies that match the provided spec.
+ *
+ * @param spec
+ * @return
+ */
+ DependencyFilter exclude(Spec<? super ResolvedDependency> spec)
+
+ /**
+ * Include dependencies that match the provided spec.
+ *
+ * @param spec
+ * @return
+ */
+ DependencyFilter include(Spec<? super ResolvedDependency> spec)
+
+ /**
+ * Create a spec that matches the provided project notation on group, name, and version
+ * @param notation
+ * @return
+ */
+ Spec<? super ResolvedDependency> project(Map<String, ?> notation)
+
+ /**
+ * Create a spec that matches the default configuration for the provided project path on group, name, and version
+ *
+ * @param notation
+ * @return
+ */
+ Spec<? super ResolvedDependency> project(String notation)
+
+ /**
+ * Create a spec that matches dependencies using the provided notation on group, name, and version
+ * @param notation
+ * @return
+ */
+ Spec<? super ResolvedDependency> dependency(Object notation)
+
+ /**
+ * Create a spec that matches the provided dependency on group, name, and version
+ * @param dependency
+ * @return
+ */
+ Spec<? super ResolvedDependency> dependency(Dependency dependency)
+
+ /**
+ * Create a spec that matches the provided closure
+ * @param spec
+ * @return
+ */
+ Spec<? super ResolvedDependency> dependency(Closure spec)
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/GradleVersionUtil.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/GradleVersionUtil.groovy
new file mode 100644
index 0000000..bcc3043
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/GradleVersionUtil.groovy
@@ -0,0 +1,62 @@
+package com.github.jengelman.gradle.plugins.shadow.internal
+
+import com.github.jengelman.gradle.plugins.shadow.internal.gradle111.Gradle111DefaultZipCompressor
+import org.apache.tools.zip.ZipOutputStream
+import org.gradle.api.internal.file.copy.CopySpecInternal
+import org.gradle.api.tasks.bundling.Jar
+import org.gradle.api.tasks.bundling.ZipEntryCompression
+import org.gradle.api.tasks.util.PatternSet
+import org.gradle.util.GradleVersion
+
+class GradleVersionUtil {
+
+ private final GradleVersion version
+
+ GradleVersionUtil(String version) {
+ this.version = GradleVersion.version(version)
+ }
+
+ PatternSet getRootPatternSet(CopySpecInternal mainSpec) {
+ // Gradle 1.12 class exposes patternSet on the spec
+ if (isGradle1x()) {
+ return mainSpec.getPatternSet()
+ // Gradle 2.x moves it to the spec resolver.
+ } else {
+ return mainSpec.buildRootResolver().getPatternSet()
+ }
+ }
+
+ ZipCompressor getInternalCompressor(ZipEntryCompression entryCompression, Jar jar) {
+ if (isGradle1_11()) {
+ return getGradle1_11InternalCompressor(entryCompression, jar)
+ } else {
+ switch (entryCompression) {
+ case ZipEntryCompression.DEFLATED:
+ return new DefaultZipCompressor(jar.zip64, ZipOutputStream.DEFLATED);
+ case ZipEntryCompression.STORED:
+ return new DefaultZipCompressor(jar.zip64, ZipOutputStream.STORED);
+ default:
+ throw new IllegalArgumentException(String.format("Unknown Compression type %s", entryCompression));
+ }
+ }
+ }
+
+ private ZipCompressor getGradle1_11InternalCompressor(ZipEntryCompression entryCompression, Jar jar) {
+ switch(entryCompression) {
+ case ZipEntryCompression.DEFLATED:
+ return Gradle111DefaultZipCompressor.INSTANCE;
+ case ZipEntryCompression.STORED:
+ return Gradle111DefaultZipCompressor.INSTANCE;
+ default:
+ throw new IllegalArgumentException(String.format("Unknown Compression type %s", entryCompression));
+ }
+ }
+
+ private boolean isGradle1x() {
+ version < GradleVersion.version('2.0')
+ }
+
+ private boolean isGradle1_11() {
+ version <= GradleVersion.version('1.11')
+ }
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/JavaJarExec.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/JavaJarExec.groovy
new file mode 100644
index 0000000..778868d
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/JavaJarExec.groovy
@@ -0,0 +1,21 @@
+package com.github.jengelman.gradle.plugins.shadow.internal
+
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.JavaExec
+import org.gradle.api.tasks.Optional
+import org.gradle.api.tasks.TaskAction
+
+class JavaJarExec extends JavaExec {
+
+ @InputFile
+ File jarFile
+
+ @Override
+ @TaskAction
+ public void exec() {
+ setMain('-jar')
+ List<String> allArgs = [getJarFile().path] + getArgs()
+ setArgs(allArgs)
+ super.exec()
+ }
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/StartScriptGenerator.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/StartScriptGenerator.groovy
new file mode 100644
index 0000000..6a1a40c
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/StartScriptGenerator.groovy
@@ -0,0 +1,140 @@
+package com.github.jengelman.gradle.plugins.shadow.internal
+
+import groovy.text.SimpleTemplateEngine
+import org.apache.tools.ant.taskdefs.Chmod
+import org.gradle.util.AntUtil
+import org.gradle.util.GFileUtils
+import org.gradle.util.TextUtil
+
+class StartScriptGenerator {
+ /**
+ * The display name of the application
+ */
+ String applicationName
+
+ /**
+ * The environment variable to use to provide additional options to the JVM
+ */
+ String optsEnvironmentVar
+
+ /**
+ * The environment variable to use to control exit value (windows only)
+ */
+ String exitEnvironmentVar
+
+ String mainApplicationJar
+
+ Iterable<String> defaultJvmOpts = []
+
+ /**
+ * The path of the script, relative to the application home directory.
+ */
+ String scriptRelPath
+
+ /**
+ * This system property to use to pass the script name to the application. May be null.
+ */
+ String appNameSystemProperty
+
+ private final engine = new SimpleTemplateEngine()
+
+ void generateUnixScript(File unixScript) {
+ String nativeOutput = generateUnixScriptContent()
+ writeToFile(nativeOutput, unixScript)
+ createExecutablePermission(unixScript)
+ }
+
+ String generateUnixScriptContent() {
+ def quotedDefaultJvmOpts = defaultJvmOpts.collect{
+ //quote ', ", \, $. Probably not perfect. TODO: identify non-working cases, fail-fast on them
+ it = it.replace('\\', '\\\\')
+ it = it.replace('"', '\\"')
+ it = it.replace(/'/, /'"'"'/)
+ it = it.replace(/`/, /'"`"'/)
+ it = it.replace('$', '\\$')
+ (/"${it}"/)
+ }
+ //put the whole arguments string in single quotes, unless defaultJvmOpts was empty,
+ // in which case we output "" to stay compatible with existing builds that scan the script for it
+ def defaultJvmOptsString = (quotedDefaultJvmOpts ? /'${quotedDefaultJvmOpts.join(' ')}'/ : '""')
+ def mainJarPath = "\$APP_HOME/${mainApplicationJar.replace('\\', '/')}"
+ def binding = [applicationName: applicationName,
+ optsEnvironmentVar: optsEnvironmentVar,
+ mainApplicationJar: mainJarPath,
+ defaultJvmOpts: defaultJvmOptsString,
+ appNameSystemProperty: appNameSystemProperty,
+ appHomeRelativePath: appHomeRelativePath]
+ return generateNativeOutput('unixStartScript.txt', binding, TextUtil.unixLineSeparator)
+ }
+
+ void generateWindowsScript(File windowsScript) {
+ String nativeOutput = generateWindowsScriptContent()
+ writeToFile(nativeOutput, windowsScript);
+ }
+
+ String generateWindowsScriptContent() {
+ def appHome = appHomeRelativePath.replace('/', '\\')
+ //argument quoting:
+ // - " must be encoded as \"
+ // - % must be encoded as %%
+ // - pathological case: \" must be encoded as \\\", but other than that, \ MUST NOT be quoted
+ // - other characters (including ') will not be quoted
+ // - use a state machine rather than regexps
+ def quotedDefaultJvmOpts = defaultJvmOpts.collect {
+ def wasOnBackslash = false
+ it = it.collect { ch ->
+ def repl = ch
+ if (ch == '%') {
+ repl = '%%'
+ } else if (ch == '"') {
+ repl = (wasOnBackslash ? '\\' : '') + '\\"'
+ }
+ wasOnBackslash = (ch == '\\')
+ repl
+ }
+ (/"${it.join()}"/)
+ }
+ def defaultJvmOptsString = quotedDefaultJvmOpts.join(' ')
+ def mainJarPath = "%APP_HOME%\\${mainApplicationJar.replace('/', '\\')}"
+ def binding = [applicationName: applicationName,
+ optsEnvironmentVar: optsEnvironmentVar,
+ exitEnvironmentVar: exitEnvironmentVar,
+ mainApplicationJar: mainJarPath,
+ defaultJvmOpts: defaultJvmOptsString,
+ appNameSystemProperty: appNameSystemProperty,
+ appHomeRelativePath: appHome]
+ return generateNativeOutput('windowsStartScript.txt', binding, TextUtil.windowsLineSeparator)
+
+ }
+
+ private void createExecutablePermission(File unixScriptFile) {
+ Chmod chmod = new Chmod()
+ chmod.file = unixScriptFile
+ chmod.perm = "ugo+rx"
+ chmod.project = AntUtil.createProject()
+ chmod.execute()
+ }
+
+ void writeToFile(String scriptContent, File scriptFile) {
+ GFileUtils.mkdirs(scriptFile.parentFile)
+ scriptFile.write(scriptContent)
+ }
+
+
+ private String generateNativeOutput(String templateName, Map binding, String lineSeparator) {
+ def stream = StartScriptGenerator.getResource(templateName)
+ def templateText = stream.text
+ def output = engine.createTemplate(templateText).make(binding)
+ def nativeOutput = TextUtil.convertLineSeparators(output as String, lineSeparator)
+ return nativeOutput;
+
+ }
+
+ private String getAppHomeRelativePath() {
+ def depth = scriptRelPath.count("/")
+ if (depth == 0) {
+ return ""
+ }
+ return (1..depth).collect {".."}.join("/")
+ }
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/ZipCompressor.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/ZipCompressor.groovy
new file mode 100644
index 0000000..5bd6a50
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/ZipCompressor.groovy
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+package com.github.jengelman.gradle.plugins.shadow.internal
+
+import org.apache.tools.zip.ZipOutputStream
+import org.gradle.api.internal.file.archive.compression.ArchiveOutputStreamFactory
+
+public interface ZipCompressor extends ArchiveOutputStreamFactory {
+
+ ZipOutputStream createArchiveOutputStream(File destination)
+
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/gradle111/Gradle111DefaultZipCompressor.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/gradle111/Gradle111DefaultZipCompressor.groovy
new file mode 100644
index 0000000..62af58b
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/gradle111/Gradle111DefaultZipCompressor.groovy
@@ -0,0 +1,31 @@
+package com.github.jengelman.gradle.plugins.shadow.internal.gradle111
+
+import com.github.jengelman.gradle.plugins.shadow.internal.ZipCompressor
+import org.apache.tools.zip.Zip64Mode
+import org.apache.tools.zip.ZipOutputStream
+import org.gradle.api.UncheckedIOException;
+
+class Gradle111DefaultZipCompressor implements ZipCompressor {
+
+ public static final ZipCompressor INSTANCE = new Gradle111DefaultZipCompressor()
+
+ public Gradle111DefaultZipCompressor() {
+ }
+
+ public int getCompressedMethod() {
+ return ZipOutputStream.DEFLATED
+ }
+
+ public ZipOutputStream createArchiveOutputStream(File destination) {
+ try {
+ OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(destination))
+ ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)
+ zipOutputStream.setUseZip64(Zip64Mode.Never)
+ zipOutputStream.setMethod(compressedMethod)
+ return zipOutputStream;
+ } catch (Exception e) {
+ String message = String.format("Unable to create ZIP output stream for file %s.", destination)
+ throw new UncheckedIOException(message, e)
+ }
+ }
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/Relocator.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/Relocator.groovy
new file mode 100644
index 0000000..c04be07
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/Relocator.groovy
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.relocation
+
+/**
+ * @author Jason van Zyl
+ *
+ * Modified from org.apache.maven.plugins.shade.relocation.Relocator.java
+ *
+ * Modifications
+ * @author John Engelman
+ */
+interface Relocator {
+ String ROLE = Relocator.class.getName()
+
+ boolean canRelocatePath(String clazz)
+
+ String relocatePath(String clazz)
+
+ boolean canRelocateClass(String clazz)
+
+ String relocateClass(String clazz)
+
+ String applyToSourceContent(String sourceContent)
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.groovy
new file mode 100644
index 0000000..efc6563
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.groovy
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.relocation
+
+import org.codehaus.plexus.util.SelectorUtils
+
+import java.util.regex.Pattern
+
+/**
+ * @author Jason van Zyl
+ * @author Mauro Talevi
+ *
+ * Modified from org.apache.maven.plugins.shade.relocation.SimpleRelocator.java
+ *
+ * Modifications
+ * @author John Engelman
+ */
+class SimpleRelocator implements Relocator {
+
+ private final String pattern
+
+ private final String pathPattern
+
+ private final String shadedPattern
+
+ private final String shadedPathPattern
+
+ private final Set<String> includes
+
+ private final Set<String> excludes
+
+ private final boolean rawString
+
+ SimpleRelocator() {
+
+ }
+
+ SimpleRelocator(String patt, String shadedPattern, List<String> includes, List<String> excludes) {
+ this(patt, shadedPattern, includes, excludes, false)
+ }
+
+ SimpleRelocator(String patt, String shadedPattern, List<String> includes, List<String> excludes,
+ boolean rawString) {
+ this.rawString = rawString
+
+ if (rawString) {
+ this.pathPattern = patt
+ this.shadedPathPattern = shadedPattern
+
+ this.pattern = null // not used for raw string relocator
+ this.shadedPattern = null // not used for raw string relocator
+ } else {
+ if (patt == null) {
+ this.pattern = ""
+ this.pathPattern = ""
+ } else {
+ this.pattern = patt.replace('/', '.')
+ this.pathPattern = patt.replace('.', '/')
+ }
+
+ if (shadedPattern != null) {
+ this.shadedPattern = shadedPattern.replace('/', '.')
+ this.shadedPathPattern = shadedPattern.replace('.', '/')
+ } else {
+ this.shadedPattern = "hidden." + this.pattern
+ this.shadedPathPattern = "hidden/" + this.pathPattern
+ }
+ }
+
+ this.includes = normalizePatterns(includes)
+ this.excludes = normalizePatterns(excludes)
+ }
+
+ SimpleRelocator include(String pattern) {
+ this.includes.addAll normalizePatterns([pattern])
+ return this
+ }
+
+ SimpleRelocator exclude(String pattern) {
+ this.excludes.addAll normalizePatterns([pattern])
+ return this
+ }
+
+ private static Set<String> normalizePatterns(Collection<String> patterns) {
+ Set<String> normalized = null
+
+ if (patterns != null && !patterns.isEmpty()) {
+ normalized = new LinkedHashSet<String>()
+
+ for (String pattern : patterns) {
+
+ String classPattern = pattern.replace('.', '/')
+
+ normalized.add(classPattern)
+
+ if (classPattern.endsWith("/*")) {
+ String packagePattern = classPattern.substring(0, classPattern.lastIndexOf('/'))
+ normalized.add(packagePattern)
+ }
+ }
+ }
+
+ return normalized ?: []
+ }
+
+ private boolean isIncluded(String path) {
+ if (includes != null && !includes.isEmpty()) {
+ for (String include : includes) {
+ if (SelectorUtils.matchPath(include, path, true)) {
+ return true
+ }
+ }
+ return false
+ }
+ return true
+ }
+
+ private boolean isExcluded(String path) {
+ if (excludes != null && !excludes.isEmpty()) {
+ for (String exclude : excludes) {
+ if (SelectorUtils.matchPath(exclude, path, true)) {
+ return true
+ }
+ }
+ }
+ return false
+ }
+
+ boolean canRelocatePath(String path) {
+ if (rawString) {
+ return Pattern.compile(pathPattern).matcher(path).find()
+ }
+
+ if (path.endsWith(".class")) {
+ path = path.substring(0, path.length() - 6)
+ }
+
+ if (!isIncluded(path) || isExcluded(path)) {
+ return false
+ }
+
+ // Allow for annoying option of an extra / on the front of a path. See MSHADE-119 comes from getClass().getResource("/a/b/c.properties").
+ return path.startsWith(pathPattern) || path.startsWith("/" + pathPattern)
+ }
+
+ boolean canRelocateClass(String clazz) {
+ return !rawString && clazz.indexOf('/') < 0 && canRelocatePath(clazz.replace('.', '/'))
+ }
+
+ String relocatePath(String path) {
+ if (rawString) {
+ return path.replaceAll(pathPattern, shadedPathPattern)
+ } else {
+ return path.replaceFirst(pathPattern, shadedPathPattern)
+ }
+ }
+
+ String relocateClass(String clazz) {
+ return clazz.replaceFirst(pattern, shadedPattern)
+ }
+
+ String applyToSourceContent(String sourceContent) {
+ if (rawString) {
+ return sourceContent
+ } else {
+ return sourceContent.replaceAll("\\b" + pattern, shadedPattern)
+ }
+ }
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/DefaultInheritManifest.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/DefaultInheritManifest.groovy
new file mode 100644
index 0000000..d2c8bbe
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/DefaultInheritManifest.groovy
@@ -0,0 +1,92 @@
+package com.github.jengelman.gradle.plugins.shadow.tasks
+
+import org.gradle.api.internal.file.FileResolver
+import org.gradle.api.java.archives.Attributes
+import org.gradle.api.java.archives.Manifest
+import org.gradle.api.java.archives.ManifestException
+import org.gradle.api.java.archives.internal.DefaultManifest
+import org.gradle.api.java.archives.internal.DefaultManifestMergeSpec
+import org.gradle.util.ConfigureUtil
+
+class DefaultInheritManifest implements InheritManifest {
+
+ private List<DefaultManifestMergeSpec> inheritMergeSpecs = []
+
+ private final FileResolver fileResolver
+
+ private final Manifest internalManifest
+
+ DefaultInheritManifest(FileResolver fileResolver) {
+ this.internalManifest = new DefaultManifest(fileResolver)
+ this.fileResolver = fileResolver
+ }
+
+ public InheritManifest inheritFrom(Object... inheritPaths) {
+ inheritFrom(inheritPaths, null)
+ return this
+ }
+
+ public InheritManifest inheritFrom(Object inheritPaths, Closure closure) {
+ DefaultManifestMergeSpec mergeSpec = new DefaultManifestMergeSpec()
+ mergeSpec.from(inheritPaths)
+ inheritMergeSpecs.add(mergeSpec)
+ ConfigureUtil.configure(closure, mergeSpec)
+ return this
+ }
+
+ @Override
+ Attributes getAttributes() {
+ return internalManifest.getAttributes()
+ }
+
+ @Override
+ Map<String, Attributes> getSections() {
+ return internalManifest.getSections()
+ }
+
+ @Override
+ Manifest attributes(Map<String, ?> map) throws ManifestException {
+ internalManifest.attributes(map)
+ return this
+ }
+
+ @Override
+ Manifest attributes(Map<String, ?> map, String s) throws ManifestException {
+ internalManifest.attributes(map, s)
+ return this
+ }
+
+ @Override
+ public DefaultManifest getEffectiveManifest() {
+ DefaultManifest base = new DefaultManifest(fileResolver)
+ inheritMergeSpecs.each {
+ base = it.merge(base, fileResolver)
+ }
+ base.from internalManifest
+ return base.getEffectiveManifest()
+ }
+
+ @Override
+ Manifest writeTo(Writer writer) {
+ this.getEffectiveManifest().writeTo(writer)
+ return this
+ }
+
+ @Override
+ Manifest writeTo(Object o) {
+ this.getEffectiveManifest().writeTo(o)
+ return this
+ }
+
+ @Override
+ Manifest from(Object... objects) {
+ internalManifest.from(objects)
+ return this
+ }
+
+ @Override
+ Manifest from(Object o, Closure<?> closure) {
+ internalManifest.from(o, closure)
+ return this
+ }
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/InheritManifest.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/InheritManifest.groovy
new file mode 100644
index 0000000..3c134da
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/InheritManifest.groovy
@@ -0,0 +1,10 @@
+package com.github.jengelman.gradle.plugins.shadow.tasks
+
+import org.gradle.api.java.archives.Manifest
+
+public interface InheritManifest extends Manifest {
+
+ InheritManifest inheritFrom(Object... inheritPaths)
+
+ InheritManifest inheritFrom(Object inheritPaths, Closure closure)
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/KnowsTask.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/KnowsTask.groovy
new file mode 100644
index 0000000..e476c72
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/KnowsTask.groovy
@@ -0,0 +1,17 @@
+package com.github.jengelman.gradle.plugins.shadow.tasks
+
+import org.codehaus.groovy.reflection.ReflectionUtils
+import org.gradle.api.DefaultTask
+import org.gradle.api.tasks.TaskAction
+
+class KnowsTask extends DefaultTask {
+
+ static final String NAME = "knows"
+ static final String DESC = "Do you know who knows?"
+
+ @TaskAction
+ def knows() {
+ println "\nNo, The Shadow Knows...."
+ println ReflectionUtils.getCallingClass(0).getResourceAsStream("/shadowBanner.txt").text
+ }
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy
new file mode 100644
index 0000000..c9ff4c7
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy
@@ -0,0 +1,425 @@
+package com.github.jengelman.gradle.plugins.shadow.tasks
+
+import com.github.jengelman.gradle.plugins.shadow.ShadowStats
+import com.github.jengelman.gradle.plugins.shadow.impl.RelocatorRemapper
+import com.github.jengelman.gradle.plugins.shadow.internal.ZipCompressor
+import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator
+import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer
+import groovy.util.logging.Slf4j
+import org.apache.commons.io.FilenameUtils
+import org.apache.commons.io.IOUtils
+import org.apache.tools.zip.UnixStat
+import org.apache.tools.zip.Zip64RequiredException
+import org.apache.tools.zip.ZipEntry
+import org.apache.tools.zip.ZipFile
+import org.apache.tools.zip.ZipOutputStream
+import org.gradle.api.Action
+import org.gradle.api.GradleException
+import org.gradle.api.UncheckedIOException
+import org.gradle.api.file.FileCopyDetails
+import org.gradle.api.file.FileTreeElement
+import org.gradle.api.file.RelativePath
+import org.gradle.api.internal.DocumentationRegistry
+import org.gradle.api.internal.file.CopyActionProcessingStreamAction
+import org.gradle.api.internal.file.copy.CopyAction
+import org.gradle.api.internal.file.copy.CopyActionProcessingStream
+import org.gradle.api.internal.file.copy.FileCopyDetailsInternal
+import org.gradle.api.internal.tasks.SimpleWorkResult
+import org.gradle.api.specs.Spec
+import org.gradle.api.tasks.WorkResult
+import org.gradle.api.tasks.bundling.Zip
+import org.gradle.api.tasks.util.PatternSet
+import org.gradle.internal.UncheckedException
+import org.objectweb.asm.ClassReader
+import org.objectweb.asm.ClassVisitor
+import org.objectweb.asm.ClassWriter
+import org.objectweb.asm.commons.RemappingClassAdapter
+
+import java.util.zip.ZipException
+
+ at Slf4j
+public class ShadowCopyAction implements CopyAction {
+
+ private final File zipFile
+ private final ZipCompressor compressor
+ private final DocumentationRegistry documentationRegistry
+ private final List<Transformer> transformers
+ private final List<Relocator> relocators
+ private final PatternSet patternSet
+ private final ShadowStats stats
+
+ public ShadowCopyAction(File zipFile, ZipCompressor compressor, DocumentationRegistry documentationRegistry,
+ List<Transformer> transformers, List<Relocator> relocators, PatternSet patternSet,
+ ShadowStats stats) {
+
+ this.zipFile = zipFile
+ this.compressor = compressor
+ this.documentationRegistry = documentationRegistry
+ this.transformers = transformers
+ this.relocators = relocators
+ this.patternSet = patternSet
+ this.stats = stats
+ }
+
+ @Override
+ WorkResult execute(CopyActionProcessingStream stream) {
+ final ZipOutputStream zipOutStr
+
+ try {
+ zipOutStr = compressor.createArchiveOutputStream(zipFile)
+ } catch (Exception e) {
+ throw new GradleException("Could not create ZIP '${zipFile.toString()}'", e)
+ }
+
+ try {
+ withResource(zipOutStr, new Action<ZipOutputStream>() {
+ public void execute(ZipOutputStream outputStream) {
+ try {
+ stream.process(new StreamAction(outputStream, transformers, relocators, patternSet,
+ stats))
+ processTransformers(outputStream)
+ } catch (Exception e) {
+ log.error('ex', e)
+ //TODO this should not be rethrown
+ throw e
+ }
+ }
+ })
+ } catch (UncheckedIOException e) {
+ if (e.cause instanceof Zip64RequiredException) {
+ throw new Zip64RequiredException(
+ String.format("%s\n\nTo build this archive, please enable the zip64 extension.\nSee: %s",
+ e.cause.message, documentationRegistry.getDslRefForProperty(Zip, "zip64"))
+ )
+ }
+ }
+ return new SimpleWorkResult(true)
+ }
+
+ private void processTransformers(ZipOutputStream stream) {
+ transformers.each { Transformer transformer ->
+ if (transformer.hasTransformedResource()) {
+ transformer.modifyOutputStream(stream)
+ }
+ }
+ }
+
+ private static <T extends Closeable> void withResource(T resource, Action<? super T> action) {
+ try {
+ action.execute(resource);
+ } catch(Throwable t) {
+ try {
+ resource.close();
+ } catch (IOException e) {
+ // Ignored
+ }
+ throw UncheckedException.throwAsUncheckedException(t);
+ }
+
+ try {
+ resource.close();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ class StreamAction implements CopyActionProcessingStreamAction {
+
+ private final ZipOutputStream zipOutStr
+ private final List<Transformer> transformers
+ private final List<Relocator> relocators
+ private final RelocatorRemapper remapper
+ private final PatternSet patternSet
+ private final ShadowStats stats
+
+ private Set<String> visitedFiles = new HashSet<String>()
+
+ public StreamAction(ZipOutputStream zipOutStr, List<Transformer> transformers, List<Relocator> relocators,
+ PatternSet patternSet, ShadowStats stats) {
+ this.zipOutStr = zipOutStr
+ this.transformers = transformers
+ this.relocators = relocators
+ this.remapper = new RelocatorRemapper(relocators)
+ this.patternSet = patternSet
+ this.stats = stats
+ }
+
+ public void processFile(FileCopyDetailsInternal details) {
+ if (details.directory) {
+ visitDir(details)
+ } else {
+ visitFile(details)
+ }
+ }
+
+ private boolean isArchive(FileCopyDetails fileDetails) {
+ return fileDetails.relativePath.pathString.endsWith('.jar') ||
+ fileDetails.relativePath.pathString.endsWith('.zip')
+ }
+
+ private boolean recordVisit(RelativePath path) {
+ return visitedFiles.add(path.pathString)
+ }
+
+ private void visitFile(FileCopyDetails fileDetails) {
+ if (!isArchive(fileDetails)) {
+ try {
+ boolean isClass = (FilenameUtils.getExtension(fileDetails.path) == 'class')
+ if (!remapper.hasRelocators() || !isClass) {
+ if (!isTransformable(fileDetails)) {
+ String mappedPath = remapper.map(fileDetails.relativePath.pathString)
+ ZipEntry archiveEntry = new ZipEntry(mappedPath)
+ archiveEntry.setTime(fileDetails.lastModified)
+ archiveEntry.unixMode = (UnixStat.FILE_FLAG | fileDetails.mode)
+ zipOutStr.putNextEntry(archiveEntry)
+ fileDetails.copyTo(zipOutStr)
+ zipOutStr.closeEntry()
+ } else {
+ transform(fileDetails)
+ }
+ } else if (isClass) {
+ remapClass(fileDetails)
+ }
+ recordVisit(fileDetails.relativePath)
+ } catch (Exception e) {
+ throw new GradleException(String.format("Could not add %s to ZIP '%s'.", fileDetails, zipFile), e)
+ }
+ } else {
+ processArchive(fileDetails)
+ }
+ }
+
+ private void processArchive(FileCopyDetails fileDetails) {
+ stats.startJar()
+ ZipFile archive = new ZipFile(fileDetails.file)
+ List<ArchiveFileTreeElement> archiveElements = archive.entries.collect {
+ new ArchiveFileTreeElement(new RelativeArchivePath(it, fileDetails))
+ }
+ Spec<FileTreeElement> patternSpec = patternSet.getAsSpec()
+ List<ArchiveFileTreeElement> filteredArchiveElements = archiveElements.findAll { ArchiveFileTreeElement archiveElement ->
+ patternSpec.isSatisfiedBy(archiveElement)
+ }
+ filteredArchiveElements.each { ArchiveFileTreeElement archiveElement ->
+ if (archiveElement.relativePath.file) {
+ visitArchiveFile(archiveElement, archive)
+ }
+ }
+ archive.close()
+ stats.finishJar()
+ }
+
+ private void visitArchiveDirectory(RelativeArchivePath archiveDir) {
+ if (recordVisit(archiveDir)) {
+ zipOutStr.putNextEntry(archiveDir.entry)
+ zipOutStr.closeEntry()
+ }
+ }
+
+ private void visitArchiveFile(ArchiveFileTreeElement archiveFile, ZipFile archive) {
+ def archiveFilePath = archiveFile.relativePath
+ if (archiveFile.classFile || !isTransformable(archiveFile)) {
+ if (recordVisit(archiveFilePath)) {
+ if (!remapper.hasRelocators() || !archiveFile.classFile) {
+ copyArchiveEntry(archiveFilePath, archive)
+ } else {
+ remapClass(archiveFilePath, archive)
+ }
+ }
+ } else {
+ transform(archiveFile, archive)
+ }
+ }
+
+ private void addParentDirectories(RelativeArchivePath file) {
+ if (file) {
+ addParentDirectories(file.parent)
+ if (!file.file) {
+ visitArchiveDirectory(file)
+ }
+ }
+ }
+
+ private void remapClass(RelativeArchivePath file, ZipFile archive) {
+ if (file.classFile) {
+ addParentDirectories(new RelativeArchivePath(new ZipEntry(remapper.mapPath(file) + '.class'), null))
+ remapClass(archive.getInputStream(file.entry), file.pathString)
+ }
+ }
+
+ private void remapClass(FileCopyDetails fileCopyDetails) {
+ if (FilenameUtils.getExtension(fileCopyDetails.name) == 'class') {
+ remapClass(fileCopyDetails.file.newInputStream(), fileCopyDetails.path)
+ }
+ }
+
+ private void remapClass(InputStream classInputStream, String path) {
+ InputStream is = classInputStream
+ ClassReader cr = new ClassReader(is)
+
+ // We don't pass the ClassReader here. This forces the ClassWriter to rebuild the constant pool.
+ // Copying the original constant pool should be avoided because it would keep references
+ // to the original class names. This is not a problem at runtime (because these entries in the
+ // constant pool are never used), but confuses some tools such as Felix' maven-bundle-plugin
+ // that use the constant pool to determine the dependencies of a class.
+ ClassWriter cw = new ClassWriter(0)
+
+ ClassVisitor cv = new RemappingClassAdapter(cw, remapper)
+
+ try {
+ cr.accept(cv, ClassReader.EXPAND_FRAMES)
+ } catch (Throwable ise) {
+ throw new GradleException("Error in ASM processing class " + path, ise)
+ }
+
+ byte[] renamedClass = cw.toByteArray()
+
+ // Need to take the .class off for remapping evaluation
+ String mappedName = remapper.mapPath(path)
+
+ try {
+ // Now we put it back on so the class file is written out with the right extension.
+ zipOutStr.putNextEntry(new ZipEntry(mappedName + ".class"))
+ IOUtils.copyLarge(new ByteArrayInputStream(renamedClass), zipOutStr)
+ zipOutStr.closeEntry()
+ } catch (ZipException e) {
+ log.warn("We have a duplicate " + mappedName + " in source project")
+ }
+ }
+
+ private void copyArchiveEntry(RelativeArchivePath archiveFile, ZipFile archive) {
+ String mappedPath = remapper.map(archiveFile.entry.name)
+ RelativeArchivePath mappedFile = new RelativeArchivePath(new ZipEntry(mappedPath), archiveFile.details)
+ addParentDirectories(mappedFile)
+ zipOutStr.putNextEntry(mappedFile.entry)
+ IOUtils.copyLarge(archive.getInputStream(archiveFile.entry), zipOutStr)
+ zipOutStr.closeEntry()
+ }
+
+ private void visitDir(FileCopyDetails dirDetails) {
+ try {
+ // Trailing slash in name indicates that entry is a directory
+ String path = dirDetails.relativePath.pathString + '/'
+ ZipEntry archiveEntry = new ZipEntry(path)
+ archiveEntry.setTime(dirDetails.lastModified)
+ archiveEntry.unixMode = (UnixStat.DIR_FLAG | dirDetails.mode)
+ zipOutStr.putNextEntry(archiveEntry)
+ zipOutStr.closeEntry()
+ recordVisit(dirDetails.relativePath)
+ } catch (Exception e) {
+ throw new GradleException(String.format("Could not add %s to ZIP '%s'.", dirDetails, zipFile), e)
+ }
+ }
+
+ private void transform(ArchiveFileTreeElement element, ZipFile archive) {
+ transform(element, archive.getInputStream(element.relativePath.entry))
+ }
+
+ private void transform(FileCopyDetails details) {
+ transform(details, details.file.newInputStream())
+ }
+
+ private void transform(FileTreeElement element, InputStream is) {
+ String mappedPath = remapper.map(element.relativePath.pathString)
+ transformers.find { it.canTransformResource(element) }.transform(mappedPath, is, relocators)
+ }
+
+ private boolean isTransformable(FileTreeElement element) {
+ return transformers.any { it.canTransformResource(element) }
+ }
+
+ }
+
+ class RelativeArchivePath extends RelativePath {
+
+ ZipEntry entry
+ FileCopyDetails details
+
+ RelativeArchivePath(ZipEntry entry, FileCopyDetails fileDetails) {
+ super(!entry.directory, entry.name.split('/'))
+ this.entry = entry
+ this.details = fileDetails
+ }
+
+ boolean isClassFile() {
+ return lastName.endsWith('.class')
+ }
+
+ RelativeArchivePath getParent() {
+ if (!segments || segments.length == 1) {
+ return null
+ } else {
+ //Parent is always a directory so add / to the end of the path
+ String path = segments[0..-2].join('/') + '/'
+ return new RelativeArchivePath(new ZipEntry(path), null)
+ }
+ }
+ }
+
+ class ArchiveFileTreeElement implements FileTreeElement {
+
+ private final RelativeArchivePath archivePath
+
+ ArchiveFileTreeElement(RelativeArchivePath archivePath) {
+ this.archivePath = archivePath
+ }
+
+ boolean isClassFile() {
+ return archivePath.classFile
+ }
+
+ @Override
+ File getFile() {
+ return null
+ }
+
+ @Override
+ boolean isDirectory() {
+ return archivePath.entry.directory
+ }
+
+ @Override
+ long getLastModified() {
+ return archivePath.entry.lastModifiedDate.time
+ }
+
+ @Override
+ long getSize() {
+ return archivePath.entry.size
+ }
+
+ @Override
+ InputStream open() {
+ return null
+ }
+
+ @Override
+ void copyTo(OutputStream outputStream) {
+
+ }
+
+ @Override
+ boolean copyTo(File file) {
+ return false
+ }
+
+ @Override
+ String getName() {
+ return archivePath.pathString
+ }
+
+ @Override
+ String getPath() {
+ return archivePath.lastName
+ }
+
+ @Override
+ RelativeArchivePath getRelativePath() {
+ return archivePath
+ }
+
+ @Override
+ int getMode() {
+ return archivePath.entry.unixMode
+ }
+ }
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCreateStartScripts.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCreateStartScripts.groovy
new file mode 100644
index 0000000..1fca056
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCreateStartScripts.groovy
@@ -0,0 +1,88 @@
+package com.github.jengelman.gradle.plugins.shadow.tasks
+
+import com.github.jengelman.gradle.plugins.shadow.internal.StartScriptGenerator
+import org.gradle.api.internal.ConventionTask
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.Optional
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.TaskAction
+import org.gradle.util.GUtil
+
+class ShadowCreateStartScripts extends ConventionTask {
+ /**
+ * The directory to write the scripts into.
+ */
+ File outputDir
+
+ /**
+ * The application's default JVM options.
+ */
+ @Input
+ @Optional
+ Iterable<String> defaultJvmOpts = []
+
+ /**
+ * The application's name.
+ */
+ @Input
+ String applicationName
+
+ String optsEnvironmentVar
+
+ String exitEnvironmentVar
+
+ /**
+ * The main application jar.
+ */
+ @InputFile
+ File mainApplicationJar
+
+ /**
+ * Returns the name of the application's OPTS environment variable.
+ */
+ @Input
+ String getOptsEnvironmentVar() {
+ if (optsEnvironmentVar) {
+ return optsEnvironmentVar
+ }
+ if (!getApplicationName()) {
+ return null
+ }
+ return "${GUtil.toConstant(getApplicationName())}_OPTS"
+ }
+
+ @Input
+ String getExitEnvironmentVar() {
+ if (exitEnvironmentVar) {
+ return exitEnvironmentVar
+ }
+ if (!getApplicationName()) {
+ return null
+ }
+ return "${GUtil.toConstant(getApplicationName())}_EXIT_CONSOLE"
+ }
+
+ @OutputFile
+ File getUnixScript() {
+ return new File(getOutputDir(), getApplicationName())
+ }
+
+ @OutputFile
+ File getWindowsScript() {
+ return new File(getOutputDir(), "${getApplicationName()}.bat")
+ }
+
+ @TaskAction
+ void generate() {
+ def generator = new StartScriptGenerator()
+ generator.applicationName = getApplicationName()
+ generator.mainApplicationJar = "lib/${getMainApplicationJar().name}"
+ generator.defaultJvmOpts = getDefaultJvmOpts()
+ generator.optsEnvironmentVar = getOptsEnvironmentVar()
+ generator.exitEnvironmentVar = getExitEnvironmentVar()
+ generator.scriptRelPath = "bin/${getUnixScript().name}"
+ generator.generateUnixScript(getUnixScript())
+ generator.generateWindowsScript(getWindowsScript())
+ }
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java
new file mode 100644
index 0000000..82365a5
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java
@@ -0,0 +1,327 @@
+package com.github.jengelman.gradle.plugins.shadow.tasks;
+
+import com.github.jengelman.gradle.plugins.shadow.ShadowStats;
+import com.github.jengelman.gradle.plugins.shadow.internal.DefaultDependencyFilter;
+import com.github.jengelman.gradle.plugins.shadow.internal.DependencyFilter;
+import com.github.jengelman.gradle.plugins.shadow.internal.GradleVersionUtil;
+import com.github.jengelman.gradle.plugins.shadow.internal.ZipCompressor;
+import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator;
+import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator;
+import com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer;
+import com.github.jengelman.gradle.plugins.shadow.transformers.GroovyExtensionModuleTransformer;
+import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer;
+import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer;
+import groovy.lang.MetaClass;
+import org.codehaus.groovy.runtime.InvokerHelper;
+import org.gradle.api.Action;
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.file.FileCollection;
+import org.gradle.api.internal.DocumentationRegistry;
+import org.gradle.api.internal.file.FileResolver;
+import org.gradle.api.internal.file.copy.CopyAction;
+import org.gradle.api.java.archives.Manifest;
+import org.gradle.api.tasks.InputFiles;
+import org.gradle.api.tasks.Optional;
+import org.gradle.api.tasks.TaskAction;
+import org.gradle.api.tasks.bundling.Jar;
+import org.gradle.api.tasks.util.PatternSet;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ShadowJar extends Jar implements ShadowSpec {
+
+ private List<Transformer> transformers;
+ private List<Relocator> relocators;
+ private List<Configuration> configurations;
+ private DependencyFilter dependencyFilter;
+
+ private final ShadowStats shadowStats = new ShadowStats();
+ private final GradleVersionUtil versionUtil;
+
+ public ShadowJar() {
+ versionUtil = new GradleVersionUtil(getProject().getGradle().getGradleVersion());
+ dependencyFilter = new DefaultDependencyFilter(getProject());
+ setManifest(new DefaultInheritManifest(getServices().get(FileResolver.class)));
+ transformers = new ArrayList<Transformer>();
+ relocators = new ArrayList<Relocator>();
+ configurations = new ArrayList<Configuration>();
+ }
+
+ @Override
+ public InheritManifest getManifest() {
+ return (InheritManifest) super.getManifest();
+ }
+
+ @Override
+ protected CopyAction createCopyAction() {
+ DocumentationRegistry documentationRegistry = getServices().get(DocumentationRegistry.class);
+ return new ShadowCopyAction(getArchivePath(), getInternalCompressor(), documentationRegistry,
+ transformers, relocators, getRootPatternSet(), shadowStats);
+ }
+
+ protected ZipCompressor getInternalCompressor() {
+ return versionUtil.getInternalCompressor(getEntryCompression(), this);
+ }
+
+ @TaskAction
+ protected void copy() {
+ from(getIncludedDependencies());
+ super.copy();
+ getLogger().info(shadowStats.toString());
+ }
+
+ @InputFiles @Optional
+ public FileCollection getIncludedDependencies() {
+ return dependencyFilter.resolve(configurations);
+ }
+
+ /**
+ * Utility method for assisting between changes in Gradle 1.12 and 2.x
+ * @return
+ */
+ protected PatternSet getRootPatternSet() {
+ return versionUtil.getRootPatternSet(getMainSpec());
+ }
+
+ /**
+ * Configure inclusion/exclusion of module & project dependencies into uber jar
+ * @param c
+ * @return
+ */
+ public ShadowJar dependencies(Action<DependencyFilter> c) {
+ if (c != null) {
+ c.execute(dependencyFilter);
+ }
+ return this;
+ }
+
+ /**
+ * Add a Transformer instance for modifying JAR resources and configure.
+ * @param clazz
+ * @return
+ */
+ public ShadowJar transform(Class<? extends Transformer> clazz) throws InstantiationException, IllegalAccessException {
+ return transform(clazz, null);
+ }
+
+ /**
+ * Add a Transformer instance for modifying JAR resources and configure.
+ * @param clazz
+ * @param c
+ * @return
+ */
+ public <T extends Transformer> ShadowJar transform(Class<T> clazz, Action<T> c) throws InstantiationException, IllegalAccessException {
+ T transformer = clazz.newInstance();
+ if (c != null) {
+ c.execute(transformer);
+ }
+ transformers.add(transformer);
+ return this;
+ }
+
+ /**
+ * Add a preconfigured transformer instance
+ * @param transformer
+ * @return
+ */
+ public ShadowJar transform(Transformer transformer) {
+ transformers.add(transformer);
+ return this;
+ }
+
+ /**
+ * Syntactic sugar for merging service files in JARs
+ * @return
+ */
+ public ShadowJar mergeServiceFiles() {
+ try {
+ transform(ServiceFileTransformer.class);
+ } catch (IllegalAccessException e) {
+ } catch (InstantiationException e) {
+ }
+ return this;
+ }
+
+ /**
+ * Syntactic sugar for merging service files in JARs
+ * @return
+ */
+ public ShadowJar mergeServiceFiles(final String rootPath) {
+ try {
+ transform(ServiceFileTransformer.class, new Action<ServiceFileTransformer>() {
+
+ @Override
+ public void execute(ServiceFileTransformer serviceFileTransformer) {
+ serviceFileTransformer.setPath(rootPath);
+ }
+ });
+ } catch (IllegalAccessException e) {
+ } catch (InstantiationException e) {
+ }
+ return this;
+ }
+
+ /**
+ * Syntactic sugar for merging service files in JARs
+ * @return
+ */
+ public ShadowJar mergeServiceFiles(Action<ServiceFileTransformer> configureClosure) {
+ try {
+ transform(ServiceFileTransformer.class, configureClosure);
+ } catch (IllegalAccessException e) {
+ } catch (InstantiationException e) {
+ }
+ return this;
+ }
+
+ /**
+ * Syntactic sugar for merging Groovy extension module descriptor files in JARs
+ * @return
+ */
+ public ShadowJar mergeGroovyExtensionModules() {
+ try {
+ transform(GroovyExtensionModuleTransformer.class);
+ } catch (IllegalAccessException e) {
+ } catch (InstantiationException e) {
+ }
+ return this;
+ }
+
+ /**
+ * Syntax sugar for merging service files in JARs
+ * @return
+ */
+ public ShadowJar append(final String resourcePath) {
+ try {
+ transform(AppendingTransformer.class, new Action<AppendingTransformer>() {
+ @Override
+ public void execute(AppendingTransformer transformer) {
+ transformer.setResource(resourcePath);
+ }
+ });
+ } catch (IllegalAccessException e) {
+ } catch (InstantiationException e) {
+ }
+ return this;
+ }
+
+ /**
+ * Add a class relocator that maps each class in the pattern to the provided destination
+ * @param pattern
+ * @param destination
+ * @return
+ */
+ public ShadowJar relocate(String pattern, String destination) {
+ return relocate(pattern, destination, null);
+ }
+
+ /**
+ * Add a class relocator that maps each class in the pattern to the provided destination
+ * @param pattern
+ * @param destination
+ * @param configure
+ * @return
+ */
+ public ShadowJar relocate(String pattern, String destination, Action<SimpleRelocator> configure) {
+ SimpleRelocator relocator = new SimpleRelocator(pattern, destination, new ArrayList<String>(), new ArrayList<String>());
+ if (configure != null) {
+ configure.execute(relocator);
+ }
+ relocators.add(relocator);
+ return this;
+ }
+
+ /**
+ * Add a relocator instance
+ * @param relocator
+ * @return
+ */
+ public ShadowJar relocate(Relocator relocator) {
+ relocators.add(relocator);
+ return this;
+ }
+
+ /**
+ * Add a relocator of the provided class and configure
+ * @param relocatorClass
+ * @return
+ */
+ public ShadowJar relocate(Class<? extends Relocator> relocatorClass) throws InstantiationException, IllegalAccessException {
+ return relocate(relocatorClass, null);
+ }
+
+ /**
+ * Add a relocator of the provided class and configure
+ * @param relocatorClass
+ * @param configure
+ * @return
+ */
+ public <R extends Relocator> ShadowJar relocate(Class<R> relocatorClass, Action<R> configure) throws InstantiationException, IllegalAccessException {
+ R relocator = relocatorClass.newInstance();
+ if (configure != null) {
+ configure.execute(relocator);
+ }
+ relocators.add(relocator);
+ return this;
+ }
+
+ public List<Transformer> getTransformers() {
+ return this.transformers;
+ }
+
+ public void setTransformers(List<Transformer> transformers) {
+ this.transformers = transformers;
+ }
+
+ public List<Relocator> getRelocators() {
+ return this.relocators;
+ }
+
+ public void setRelocators(List<Relocator> relocators) {
+ this.relocators = relocators;
+ }
+
+ public List<Configuration> getConfigurations() {
+ return this.configurations;
+ }
+
+ public void setConfigurations(List<Configuration> configurations) {
+ this.configurations = configurations;
+ }
+
+ public DependencyFilter getDependencyFilter() {
+ return this.dependencyFilter;
+ }
+
+ public void setDependencyFilter(DependencyFilter filter) {
+ this.dependencyFilter = filter;
+ }
+
+ // This code is only to make IntelliJ happy.
+ private transient MetaClass metaClass = InvokerHelper.getMetaClass(this.getClass());
+
+ public Object getProperty(String property) {
+ return this.getMetaClass().getProperty(this, property);
+ }
+
+ public void setProperty(String property, Object newValue) {
+ this.getMetaClass().setProperty(this, property, newValue);
+ }
+
+ public Object invokeMethod(String name, Object args) {
+ return this.getMetaClass().invokeMethod(this, name, args);
+ }
+
+ public MetaClass getMetaClass() {
+ if(this.metaClass == null) {
+ this.metaClass = InvokerHelper.getMetaClass(this.getClass());
+ }
+
+ return this.metaClass;
+ }
+
+ public void setMetaClass(MetaClass metaClass) {
+ this.metaClass = metaClass;
+ }
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowSpec.java b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowSpec.java
new file mode 100644
index 0000000..8874fc3
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowSpec.java
@@ -0,0 +1,40 @@
+package com.github.jengelman.gradle.plugins.shadow.tasks;
+
+import com.github.jengelman.gradle.plugins.shadow.internal.DependencyFilter;
+import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator;
+import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator;
+import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer;
+import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer;
+import org.gradle.api.Action;
+import org.gradle.api.file.CopySpec;
+
+public interface ShadowSpec extends CopySpec {
+
+ public ShadowSpec dependencies(Action<DependencyFilter> configure);
+
+ public ShadowSpec transform(Class<? extends Transformer> clazz) throws InstantiationException, IllegalAccessException;
+
+ public <T extends Transformer> ShadowSpec transform(Class<T> clazz, Action<T> configure) throws InstantiationException, IllegalAccessException;
+
+ public ShadowSpec transform(Transformer transformer);
+
+ public ShadowSpec mergeServiceFiles();
+
+ public ShadowSpec mergeServiceFiles(String rootPath);
+
+ public ShadowSpec mergeServiceFiles(Action<ServiceFileTransformer> configureClosure);
+
+ public ShadowSpec mergeGroovyExtensionModules();
+
+ public ShadowSpec append(String resourcePath);
+
+ public ShadowSpec relocate(String pattern, String destination);
+
+ public ShadowSpec relocate(String pattern, String destination, Action<SimpleRelocator> configure);
+
+ public ShadowSpec relocate(Relocator relocator);
+
+ public ShadowSpec relocate(Class<? extends Relocator> clazz) throws InstantiationException, IllegalAccessException;
+
+ public <R extends Relocator> ShadowSpec relocate(Class<R> clazz, Action<R> configure) throws InstantiationException, IllegalAccessException;
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheLicenseResourceTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheLicenseResourceTransformer.groovy
new file mode 100644
index 0000000..3e3e602
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheLicenseResourceTransformer.groovy
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.transformers
+
+import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator
+import org.apache.tools.zip.ZipOutputStream
+import org.gradle.api.file.FileTreeElement
+
+/**
+ * Prevents duplicate copies of the license
+ *
+ * Modified from org.apache.maven.plugins.shade.resouce.ApacheLicenseResourceTransformer.java
+ *
+ * Modifications
+ * @author John Engelman
+ */
+class ApacheLicenseResourceTransformer implements Transformer {
+
+ private static final String LICENSE_PATH = "META-INF/LICENSE"
+
+ private static final String LICENSE_TXT_PATH = "META-INF/LICENSE.txt"
+
+ boolean canTransformResource(FileTreeElement element) {
+ def path = element.relativePath.pathString
+ return LICENSE_PATH.equalsIgnoreCase(path) ||
+ LICENSE_TXT_PATH.regionMatches(true, 0, path, 0, LICENSE_TXT_PATH.length())
+ }
+
+ void transform(String path, InputStream is, List<Relocator> relocators) {
+
+ }
+
+ boolean hasTransformedResource() {
+ return false
+ }
+
+ void modifyOutputStream(ZipOutputStream os) {
+ }
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformer.groovy
new file mode 100644
index 0000000..2d81286
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformer.groovy
@@ -0,0 +1,207 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.transformers
+
+import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator
+import org.apache.tools.zip.ZipEntry
+import org.apache.tools.zip.ZipOutputStream
+import org.codehaus.plexus.util.StringUtils
+import org.gradle.api.file.FileTreeElement
+
+import java.text.SimpleDateFormat
+
+/**
+ * Merges <code>META-INF/NOTICE.TXT</code> files.
+ *
+ * Modified from org.apache.maven.plugins.shade.resource.ApacheNoticeResourceTransformer.javA
+ *
+ * Modifications
+ * @author John Engelman
+ */
+class ApacheNoticeResourceTransformer implements Transformer {
+ Set<String> entries = new LinkedHashSet<String>()
+
+ Map<String, Set<String>> organizationEntries = new LinkedHashMap<String, Set<String>>()
+
+ String projectName = "" // MSHADE-101 :: NullPointerException when projectName is missing
+
+ boolean addHeader = true
+
+ String preamble1 = "// ------------------------------------------------------------------\n" +
+ "// NOTICE file corresponding to the section 4d of The Apache License,\n" +
+ "// Version 2.0, in this case for "
+
+ String preamble2 = "\n// ------------------------------------------------------------------\n"
+
+ String preamble3 = "This product includes software developed at\n"
+
+ //defaults overridable via config in pom
+ String organizationName = "The Apache Software Foundation"
+
+ String organizationURL = "http://www.apache.org/"
+
+ String inceptionYear = "2006"
+
+ String copyright
+
+ /**
+ * The file encoding of the <code>NOTICE</code> file.
+ */
+ String encoding
+
+ private static final String NOTICE_PATH = "META-INF/NOTICE"
+
+ private static final String NOTICE_TXT_PATH = "META-INF/NOTICE.txt"
+
+ boolean canTransformResource(FileTreeElement element) {
+ def path = element.relativePath.pathString
+ if (NOTICE_PATH.equalsIgnoreCase(path) || NOTICE_TXT_PATH.equalsIgnoreCase(path)) {
+ return true
+ }
+
+ return false
+ }
+
+ void transform(String path, InputStream is, List<Relocator> relocators) {
+ if (entries.isEmpty()) {
+ String year = new SimpleDateFormat("yyyy").format(new Date())
+ if (!inceptionYear.equals(year)) {
+ year = inceptionYear + "-" + year
+ }
+
+ //add headers
+ if (addHeader) {
+ entries.add(preamble1 + projectName + preamble2)
+ } else {
+ entries.add("")
+ }
+ //fake second entry, we'll look for a real one later
+ entries.add(projectName + "\nCopyright " + year + " " + organizationName + "\n")
+ entries.add(preamble3 + organizationName + " (" + organizationURL + ").\n")
+ }
+
+ BufferedReader reader
+ if (StringUtils.isNotEmpty(encoding)) {
+ reader = new BufferedReader(new InputStreamReader(is, encoding))
+ } else {
+ reader = new BufferedReader(new InputStreamReader(is))
+ }
+
+ String line = reader.readLine()
+ StringBuffer sb = new StringBuffer()
+ Set<String> currentOrg = null
+ int lineCount = 0
+ while (line != null) {
+ String trimedLine = line.trim()
+
+ if (!trimedLine.startsWith("//")) {
+ if (trimedLine.length() > 0) {
+ if (trimedLine.startsWith("- ")) {
+ //resource-bundle 1.3 mode
+ if (lineCount == 1
+ && sb.toString().indexOf("This product includes/uses software(s) developed by") != -1) {
+ currentOrg = organizationEntries.get(sb.toString().trim())
+ if (currentOrg == null) {
+ currentOrg = new TreeSet<String>()
+ organizationEntries.put(sb.toString().trim(), currentOrg)
+ }
+ sb = new StringBuffer()
+ } else if (sb.length() > 0 && currentOrg != null) {
+ currentOrg.add(sb.toString())
+ sb = new StringBuffer()
+ }
+
+ }
+ sb.append(line).append("\n")
+ lineCount++
+ } else {
+ String ent = sb.toString()
+ if (ent.startsWith(projectName) && ent.indexOf("Copyright ") != -1) {
+ copyright = ent
+ }
+ if (currentOrg == null) {
+ entries.add(ent)
+ } else {
+ currentOrg.add(ent)
+ }
+ sb = new StringBuffer()
+ lineCount = 0
+ currentOrg = null
+ }
+ }
+
+ line = reader.readLine()
+ }
+ if (sb.length() > 0) {
+ if (currentOrg == null) {
+ entries.add(sb.toString())
+ } else {
+ currentOrg.add(sb.toString())
+ }
+ }
+ }
+
+ boolean hasTransformedResource() {
+ return true
+ }
+
+ void modifyOutputStream(ZipOutputStream os) {
+ os.putNextEntry(new ZipEntry(NOTICE_PATH))
+
+ Writer pow
+ if (StringUtils.isNotEmpty(encoding)) {
+ pow = new OutputStreamWriter(os, encoding)
+ } else {
+ pow = new OutputStreamWriter(os)
+ }
+ PrintWriter writer = new PrintWriter(pow)
+
+ int count = 0
+ for (String line : entries) {
+ ++count
+ if (line.equals(copyright) && count != 2) {
+ continue
+ }
+
+ if (count == 2 && copyright != null) {
+ writer.print(copyright)
+ writer.print('\n')
+ } else {
+ writer.print(line)
+ writer.print('\n')
+ }
+ if (count == 3) {
+ //do org stuff
+ for (Map.Entry<String, Set<String>> entry : organizationEntries.entrySet()) {
+ writer.print(entry.getKey())
+ writer.print('\n')
+ for (String l : entry.getValue()) {
+ writer.print(l)
+ }
+ writer.print('\n')
+ }
+ }
+ }
+
+ writer.flush()
+
+ entries.clear()
+ }
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformer.groovy
new file mode 100644
index 0000000..862fd92
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformer.groovy
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.transformers
+
+import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator
+import org.apache.tools.zip.ZipEntry
+import org.apache.tools.zip.ZipOutputStream
+import org.codehaus.plexus.util.IOUtil
+import org.gradle.api.file.FileTreeElement
+
+/**
+ * A resource processor that appends content for a resource, separated by a newline.
+ *
+ * Modified from org.apache.maven.plugins.shade.resource.AppendingTransformer.java
+ *
+ * Modifications
+ * @author John Engelman
+ */
+class AppendingTransformer implements Transformer {
+ String resource
+
+ ByteArrayOutputStream data = new ByteArrayOutputStream()
+
+ boolean canTransformResource(FileTreeElement element) {
+ def path = element.relativePath.pathString
+ if (resource != null && resource.equalsIgnoreCase(path)) {
+ return true
+ }
+
+ return false
+ }
+
+ void transform(String path, InputStream is, List<Relocator> relocators) {
+ IOUtil.copy(is, data)
+ data.write('\n'.bytes)
+
+ is.close()
+ }
+
+ boolean hasTransformedResource() {
+ return data.size() > 0
+ }
+
+ void modifyOutputStream(ZipOutputStream os) {
+ os.putNextEntry(new ZipEntry(resource))
+
+ IOUtil.copy(new ByteArrayInputStream(data.toByteArray()), os)
+ data.reset()
+ }
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ComponentsXmlResourceTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ComponentsXmlResourceTransformer.groovy
new file mode 100644
index 0000000..3dc94bb
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ComponentsXmlResourceTransformer.groovy
@@ -0,0 +1,183 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.transformers
+
+import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator
+import org.apache.tools.zip.ZipEntry
+import org.apache.tools.zip.ZipOutputStream
+import org.gradle.api.file.FileTreeElement
+import org.codehaus.plexus.util.IOUtil
+import org.codehaus.plexus.util.ReaderFactory
+import org.codehaus.plexus.util.WriterFactory
+import org.codehaus.plexus.util.xml.Xpp3Dom
+import org.codehaus.plexus.util.xml.Xpp3DomBuilder
+import org.codehaus.plexus.util.xml.Xpp3DomWriter
+
+/**
+ * A resource processor that aggregates plexus <code>components.xml</code> files.
+ *
+ * Modified from org.apache.maven.plugins.shade.resource.ComponentsXmlResourceTransformer.java
+ *
+ * Modifications
+ * @author John Engelman
+ */
+class ComponentsXmlResourceTransformer implements Transformer {
+ private Map<String, Xpp3Dom> components = new LinkedHashMap<String, Xpp3Dom>()
+
+ static final String COMPONENTS_XML_PATH = "META-INF/plexus/components.xml"
+
+ boolean canTransformResource(FileTreeElement element) {
+ def path = element.relativePath.pathString
+ return COMPONENTS_XML_PATH.equals(path)
+ }
+
+ void transform(String path, InputStream is, List<Relocator> relocators) {
+ Xpp3Dom newDom
+
+ try {
+ BufferedInputStream bis = new BufferedInputStream(is) {
+ void close()
+ throws IOException {
+ // leave ZIP open
+ }
+ }
+
+ Reader reader = ReaderFactory.newXmlReader(bis)
+
+ newDom = Xpp3DomBuilder.build(reader)
+ }
+ catch (Exception e) {
+ throw (IOException) new IOException("Error parsing components.xml in " + is).initCause(e)
+ }
+
+ // Only try to merge in components if there are some elements in the component-set
+ if (newDom.getChild("components") == null) {
+ return
+ }
+
+ Xpp3Dom[] children = newDom.getChild("components").getChildren("component")
+
+ for (int i = 0; i < children.length; i++) {
+ Xpp3Dom component = children[i]
+
+ String role = getValue(component, "role")
+ role = getRelocatedClass(role, relocators)
+ setValue(component, "role", role)
+
+ String roleHint = getValue(component, "role-hint")
+
+ String impl = getValue(component, "implementation")
+ impl = getRelocatedClass(impl, relocators)
+ setValue(component, "implementation", impl)
+
+ String key = role + ':' + roleHint
+ if (components.containsKey(key)) {
+ // TODO: use the tools in Plexus to merge these properly. For now, I just need an all-or-nothing
+ // configuration carry over
+
+ Xpp3Dom dom = components.get(key)
+ if (dom.getChild("configuration") != null) {
+ component.addChild(dom.getChild("configuration"))
+ }
+ }
+
+ Xpp3Dom requirements = component.getChild("requirements")
+ if (requirements != null && requirements.getChildCount() > 0) {
+ for (int r = requirements.getChildCount() - 1; r >= 0; r--) {
+ Xpp3Dom requirement = requirements.getChild(r)
+
+ String requiredRole = getValue(requirement, "role")
+ requiredRole = getRelocatedClass(requiredRole, relocators)
+ setValue(requirement, "role", requiredRole)
+ }
+ }
+
+ components.put(key, component)
+ }
+ }
+
+ void modifyOutputStream(ZipOutputStream os) {
+ byte[] data = getTransformedResource()
+
+ os.putNextEntry(new ZipEntry(COMPONENTS_XML_PATH))
+
+ IOUtil.copy(data, os)
+
+ components.clear()
+ }
+
+ boolean hasTransformedResource() {
+ return !components.isEmpty()
+ }
+
+ byte[] getTransformedResource()
+ throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(1024 * 4)
+
+ Writer writer = WriterFactory.newXmlWriter(baos)
+ try {
+ Xpp3Dom dom = new Xpp3Dom("component-set")
+
+ Xpp3Dom componentDom = new Xpp3Dom("components")
+
+ dom.addChild(componentDom)
+
+ for (Xpp3Dom component : components.values()) {
+ componentDom.addChild(component)
+ }
+
+ Xpp3DomWriter.write(writer, dom)
+ }
+ finally {
+ IOUtil.close(writer)
+ }
+
+ return baos.toByteArray()
+ }
+
+ private String getRelocatedClass(String className, List<Relocator> relocators) {
+ if (className != null && className.length() > 0 && relocators != null) {
+ for (Relocator relocator : relocators) {
+ if (relocator.canRelocateClass(className)) {
+ return relocator.relocateClass(className)
+ }
+ }
+ }
+
+ return className
+ }
+
+ private static String getValue(Xpp3Dom dom, String element) {
+ Xpp3Dom child = dom.getChild(element)
+
+ return (child != null && child.getValue() != null) ? child.getValue() : ""
+ }
+
+ private static void setValue(Xpp3Dom dom, String element, String value) {
+ Xpp3Dom child = dom.getChild(element)
+
+ if (child == null || value == null || value.length() <= 0) {
+ return
+ }
+
+ child.setValue(value)
+ }
+
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/DontIncludeResourceTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/DontIncludeResourceTransformer.groovy
new file mode 100644
index 0000000..4a01031
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/DontIncludeResourceTransformer.groovy
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.transformers
+
+import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator
+import org.apache.tools.zip.ZipOutputStream
+import org.gradle.api.file.FileTreeElement
+
+/**
+ * A resource processor that prevents the inclusion of an arbitrary
+ * resource into the shaded JAR.
+ *
+ * Modified from org.apache.maven.plugins.shade.resource.DontIncludeResourceTransformer.java
+ *
+ * Modifications
+ * @author John Engelman
+ */
+class DontIncludeResourceTransformer implements Transformer {
+ String resource
+
+ boolean canTransformResource(FileTreeElement element) {
+ def path = element.relativePath.pathString
+ if (path.endsWith(resource)) {
+ return true
+ }
+
+ return false
+ }
+
+ void transform(String path, InputStream is, List<Relocator> relocators) {
+ // no op
+ }
+
+ boolean hasTransformedResource() {
+ return false
+ }
+
+ void modifyOutputStream(ZipOutputStream os) {
+ // no op
+ }
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/GroovyExtensionModuleTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/GroovyExtensionModuleTransformer.groovy
new file mode 100644
index 0000000..a3e3033
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/GroovyExtensionModuleTransformer.groovy
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.transformers
+
+import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator
+import org.apache.tools.zip.ZipEntry
+import org.apache.tools.zip.ZipOutputStream
+import org.gradle.api.file.FileTreeElement
+import org.codehaus.plexus.util.IOUtil
+
+/**
+ * Modified from eu.appsatori.gradle.fatjar.tasks.PrepareFiles.groovy
+ *
+ * Resource transformer that merges Groovy extension module descriptor files into a single file. If there are several
+ * META-INF/services/org.codehaus.groovy.runtime.ExtensionModule resources spread across many JARs the individual
+ * entries will all be merged into a single META-INF/services/org.codehaus.groovy.runtime.ExtensionModule resource
+ * packaged into the resultant JAR produced by the shadowing process.
+ */
+class GroovyExtensionModuleTransformer implements Transformer {
+
+ private static final GROOVY_EXTENSION_MODULE_DESCRIPTOR_PATH =
+ "META-INF/services/org.codehaus.groovy.runtime.ExtensionModule"
+
+ private static final MODULE_NAME_KEY = 'moduleName'
+ private static final MODULE_VERSION_KEY = 'moduleVersion'
+ private static final EXTENSION_CLASSES_KEY = 'extensionClasses'
+ private static final STATIC_EXTENSION_CLASSES_KEY = 'staticExtensionClasses'
+
+ private static final MERGED_MODULE_NAME = 'MergedByShadowJar'
+ private static final MERGED_MODULE_VERSION = '1.0.0'
+
+ private final Properties module = new Properties()
+
+ @Override
+ boolean canTransformResource(FileTreeElement element) {
+ return element.relativePath.pathString == GROOVY_EXTENSION_MODULE_DESCRIPTOR_PATH
+ }
+
+ @Override
+ void transform(String path, InputStream is, List<Relocator> relocators) {
+ def props = new Properties()
+ props.load(is)
+ props.each { String key, String value ->
+ switch (key) {
+ case MODULE_NAME_KEY:
+ handle(key, value) {
+ module.setProperty(key, MERGED_MODULE_NAME)
+ }
+ break
+ case MODULE_VERSION_KEY:
+ handle(key, value) {
+ module.setProperty(key, MERGED_MODULE_VERSION)
+ }
+ break
+ case [EXTENSION_CLASSES_KEY, STATIC_EXTENSION_CLASSES_KEY]:
+ handle(key, value) { String existingValue ->
+ def newValue = "${existingValue},${value}"
+ module.setProperty(key, newValue)
+ }
+ break
+ }
+ }
+ }
+
+ private handle(String key, String value, Closure mergeValue) {
+ def existingValue = module.getProperty(key)
+ if (existingValue) {
+ mergeValue(existingValue)
+ } else {
+ module.setProperty(key, value)
+ }
+ }
+
+ @Override
+ boolean hasTransformedResource() {
+ return module.size() > 0
+ }
+
+ @Override
+ void modifyOutputStream(ZipOutputStream os) {
+ os.putNextEntry(new ZipEntry(GROOVY_EXTENSION_MODULE_DESCRIPTOR_PATH))
+ IOUtil.copy(toInputStream(module), os)
+ os.closeEntry()
+ }
+
+ private static InputStream toInputStream(Properties props) {
+ def baos = new ByteArrayOutputStream()
+ props.store(baos, null)
+ return new ByteArrayInputStream(baos.toByteArray())
+ }
+
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/IncludeResourceTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/IncludeResourceTransformer.groovy
new file mode 100644
index 0000000..ac5b34d
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/IncludeResourceTransformer.groovy
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.transformers
+
+import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator
+import org.apache.tools.zip.ZipEntry
+import org.apache.tools.zip.ZipOutputStream
+import org.gradle.api.file.FileTreeElement
+import org.codehaus.plexus.util.IOUtil
+
+/**
+ * A resource processor that allows the addition of an arbitrary file
+ * content into the shaded JAR.
+ *
+ * Modified from org.apache.maven.plugins.shade.resource.IncludeResourceTransformer.java
+ *
+ * Modifications
+ * @author John Engelman
+ */
+public class IncludeResourceTransformer implements Transformer {
+ File file
+
+ String resource
+
+ public boolean canTransformResource(FileTreeElement element) {
+ return false
+ }
+
+ public void transform(String path, InputStream is, List<Relocator> relocators) {
+ // no op
+ }
+
+ public boolean hasTransformedResource() {
+ return file != null ? file.exists() : false
+ }
+
+ public void modifyOutputStream(ZipOutputStream os) {
+ os.putNextEntry(new ZipEntry(resource))
+
+ InputStream is = new FileInputStream(file)
+ IOUtil.copy(is, os)
+ is.close()
+ }
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ManifestResourceTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ManifestResourceTransformer.groovy
new file mode 100644
index 0000000..967c2d8
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ManifestResourceTransformer.groovy
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.transformers
+
+import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator
+import org.apache.tools.zip.ZipEntry
+import org.apache.tools.zip.ZipOutputStream
+import org.gradle.api.file.FileTreeElement
+import org.codehaus.plexus.util.IOUtil
+
+import java.util.jar.*
+import java.util.jar.Attributes.Name
+
+/**
+ * A resource processor that allows the arbitrary addition of attributes to
+ * the first MANIFEST.MF that is found in the set of JARs being processed, or
+ * to a newly created manifest for the shaded JAR.
+ *
+ * @author Jason van Zyl
+ * @since 1.2
+ *
+ * Modified from org.apache.maven.plugins.shade.resource.ManifestResourceTransformer
+ *
+ * Modifications
+ * @author John Engelman
+ */
+class ManifestResourceTransformer implements Transformer {
+
+ // Configuration
+ private String mainClass
+
+ private Map<String, Attributes> manifestEntries
+
+ // Fields
+ private boolean manifestDiscovered
+
+ private Manifest manifest
+
+ boolean canTransformResource(FileTreeElement element) {
+ def path = element.relativePath.pathString
+ if (JarFile.MANIFEST_NAME.equalsIgnoreCase(path)) {
+ return true
+ }
+
+ return false
+ }
+
+ void transform(String path, InputStream is, List<Relocator> relocators) {
+ // We just want to take the first manifest we come across as that's our project's manifest. This is the behavior
+ // now which is situational at best. Right now there is no context passed in with the processing so we cannot
+ // tell what artifact is being processed.
+ if (!manifestDiscovered) {
+ manifest = new Manifest(is)
+ manifestDiscovered = true
+ IOUtil.close(is)
+ }
+ }
+
+ boolean hasTransformedResource() {
+ return true
+ }
+
+ void modifyOutputStream(ZipOutputStream os) {
+ // If we didn't find a manifest, then let's create one.
+ if (manifest == null) {
+ manifest = new Manifest()
+ }
+
+ Attributes attributes = manifest.getMainAttributes()
+
+ if (mainClass != null) {
+ attributes.put(Name.MAIN_CLASS, mainClass)
+ }
+
+ if (manifestEntries != null) {
+ for (Map.Entry<String, Attributes> entry : manifestEntries.entrySet()) {
+ attributes.put(new Name(entry.getKey()), entry.getValue())
+ }
+ }
+
+ os.putNextEntry(new ZipEntry(JarFile.MANIFEST_NAME))
+ manifest.write(os)
+ }
+
+ ManifestResourceTransformer attributes(Map<String, ?> attributes) {
+ if (manifestEntries == null) {
+ manifestEntries = [:]
+ }
+ manifestEntries.putAll(attributes)
+ this
+ }
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer.groovy
new file mode 100644
index 0000000..94d0cec
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer.groovy
@@ -0,0 +1,209 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.transformers
+
+import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator
+import org.apache.tools.zip.ZipEntry
+import org.apache.tools.zip.ZipOutputStream
+import org.gradle.api.file.FileTreeElement
+import org.codehaus.plexus.util.IOUtil
+
+/**
+ * Resources transformer that merges Properties files.
+ *
+ * <p>The default merge strategy discards duplicate values coming from additional
+ * resources. This behavior can be changed by setting a value for the <tt>mergeStrategy</tt>
+ * property, such as 'first' (default), 'latest' or 'append'. If the merge strategy is
+ * 'latest' then the last value of a matching property entry will be used. If the
+ * merge strategy is 'append' then the property values will be combined, using a
+ * merge separator (default value is ','). The merge separator can be changed by
+ * setting a value for the <tt>mergeSeparator</tt> property.</p>
+ *
+ * Say there are two properties files A and B with the
+ * following entries:
+ *
+ * <strong>A</strong>
+ * <ul>
+ * <li>key1 = value1</li>
+ * <li>key2 = value2</li>
+ * </ul>
+ *
+ * <strong>B</strong>
+ * <ul>
+ * <li>key2 = balue2</li>
+ * <li>key3 = value3</li>
+ * </ul>
+ *
+ * With <tt>mergeStrategy = first</tt> you get
+ *
+ * <strong>C</strong>
+ * <ul>
+ * <li>key1 = value1</li>
+ * <li>key2 = value2</li>
+ * <li>key3 = value3</li>
+ * </ul>
+ *
+ * With <tt>mergeStrategy = latest</tt> you get
+ *
+ * <strong>C</strong>
+ * <ul>
+ * <li>key1 = value1</li>
+ * <li>key2 = balue2</li>
+ * <li>key3 = value3</li>
+ * </ul>
+ *
+ * With <tt>mergeStrategy = append</tt> and <tt>mergeSparator = ;</tt> you get
+ *
+ * <strong>C</strong>
+ * <ul>
+ * <li>key1 = value1</li>
+ * <li>key2 = value2;balue2</li>
+ * <li>key3 = value3</li>
+ * </ul>
+ *
+ * <p>There are two additional properties that can be set: <tt>paths</tt> and <tt>mappings</tt>.
+ * The first contains a list of strings or regexes that will be used to determine if
+ * a path should be transformed or not. The merge strategy and merge separator are
+ * taken from the global settings.</p>
+ *
+ * <p>The <tt>mappings</tt> property allows you to define merge strategy and separator per
+ * path</p>. If either <tt>paths</tt> or <tt>mappings</tt> is defined then no other path
+ * entries will be merged. <tt>mappings</tt> has precedence over <tt>paths</tt> if both
+ * are defined.</p>
+ *
+ * <p>Example:</p>
+ * <pre>
+ * import org.codehaus.griffon.gradle.shadow.transformers.*
+ * shadowJar {
+ * transform(PropertiesFileTransformer) {
+ * paths = [
+ * 'META-INF/editors/java.beans.PropertyEditor'
+ * ]
+ * }
+ * }
+ * </pre>
+ *
+ * @author Andres Almiray
+ */
+class PropertiesFileTransformer implements Transformer {
+ private static final String PROPERTIES_SUFFIX = '.properties'
+
+ // made public for testing
+ Map<String, Properties> propertiesEntries = [:]
+
+ // Transformer properties
+ List<String> paths = []
+ Map<String, Map<String, String>> mappings = [:]
+ String mergeStrategy = 'first' // latest, append
+ String mergeSeparator = ','
+
+ @Override
+ boolean canTransformResource(FileTreeElement element) {
+ def path = element.relativePath.pathString
+ if (mappings.containsKey(path)) return true
+ for (key in mappings.keySet()) {
+ if (path =~ /$key/) return true
+ }
+
+ if (path in paths) return true
+ for (p in paths) {
+ if (path =~ /$p/) return true
+ }
+
+ !mappings && !paths && path.endsWith(PROPERTIES_SUFFIX)
+ }
+
+ @Override
+ void transform(String path, InputStream is, List<Relocator> relocators) {
+ Properties props = propertiesEntries[path]
+ if (props == null) {
+ props = new Properties()
+ props.load(is)
+ propertiesEntries[path] = props
+ } else {
+ Properties incoming = new Properties()
+ incoming.load(is)
+ incoming.each { key, value ->
+ if (props.containsKey(key)) {
+ switch (mergeStrategyFor(path).toLowerCase()) {
+ case 'latest':
+ props.put(key, value)
+ break
+ case 'append':
+ props.put(key, props.getProperty(key) + mergeSeparatorFor(path) + value)
+ break
+ case 'first':
+ default:
+ // continue
+ break
+ }
+ } else {
+ props.put(key, value)
+ }
+ }
+ }
+ }
+
+ private String mergeStrategyFor(String path) {
+ if (mappings.containsKey(path)) {
+ return mappings.get(path).mergeStrategy ?: mergeStrategy
+ }
+ for (key in mappings.keySet()) {
+ if (path =~ /$key/) {
+ return mappings.get(key).mergeStrategy ?: mergeStrategy
+ }
+ }
+
+ return mergeStrategy
+ }
+
+ private String mergeSeparatorFor(String path) {
+ if (mappings.containsKey(path)) {
+ return mappings.get(path).mergeSeparator ?: mergeSeparator
+ }
+ for (key in mappings.keySet()) {
+ if (path =~ /$key/) {
+ return mappings.get(key).mergeSeparator ?: mergeSeparator
+ }
+ }
+
+ return mergeSeparator
+ }
+
+ @Override
+ boolean hasTransformedResource() {
+ propertiesEntries.size() > 0
+ }
+
+ @Override
+ void modifyOutputStream(ZipOutputStream os) {
+ propertiesEntries.each { String path, Properties props ->
+ os.putNextEntry(new ZipEntry(path))
+ IOUtil.copy(toInputStream(props), os)
+ os.closeEntry()
+ }
+ }
+
+ private static InputStream toInputStream(Properties props) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream()
+ props.store(baos, '')
+ new ByteArrayInputStream(baos.toByteArray())
+ }
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformer.groovy
new file mode 100644
index 0000000..0f6a326
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformer.groovy
@@ -0,0 +1,223 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.transformers
+
+import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator
+import org.apache.tools.zip.ZipEntry
+import org.apache.tools.zip.ZipOutputStream
+import org.gradle.api.file.FileTreeElement
+import org.gradle.api.specs.Spec
+import org.gradle.api.tasks.util.PatternFilterable
+import org.gradle.api.tasks.util.PatternSet
+import org.codehaus.plexus.util.IOUtil
+
+/**
+ * Modified from org.apache.maven.plugins.shade.resource.ServiceResourceTransformer.java
+ *
+ * Resources transformer that appends entries in META-INF/services resources into
+ * a single resource. For example, if there are several META-INF/services/org.apache.maven.project.ProjectBuilder
+ * resources spread across many JARs the individual entries will all be concatenated into a single
+ * META-INF/services/org.apache.maven.project.ProjectBuilder resource packaged into the resultant JAR produced
+ * by the shading process.
+ *
+ * Original
+ * @author jvanzyl
+ *
+ * Modifications
+ * @author Charlie Knudsen
+ * @author John Engelman
+ */
+class ServiceFileTransformer implements Transformer, PatternFilterable {
+
+ private static final String SERVICES_PATTERN = "META-INF/services/**"
+
+ private static final String GROOVY_EXTENSION_MODULE_DESCRIPTOR_PATTERN =
+ "META-INF/services/org.codehaus.groovy.runtime.ExtensionModule"
+
+ Map<String, ServiceStream> serviceEntries = [:].withDefault { new ServiceStream() }
+
+ private final PatternSet patternSet =
+ new PatternSet().include(SERVICES_PATTERN).exclude(GROOVY_EXTENSION_MODULE_DESCRIPTOR_PATTERN)
+
+ void setPath(String path) {
+ patternSet.setIncludes(["${path}/**"])
+ }
+
+ @Override
+ boolean canTransformResource(FileTreeElement element) {
+ return patternSet.asSpec.isSatisfiedBy(element)
+ }
+
+ @Override
+ void transform(String path, InputStream is, List<Relocator> relocators) {
+ def lines = is.readLines()
+ relocators.each {rel ->
+ if(rel.canRelocateClass(new File(path).name)) {
+ path = rel.relocateClass(path)
+ }
+ lines.eachWithIndex { String line, int i ->
+ if(rel.canRelocateClass(line)) {
+ lines[i] = rel.relocateClass(line)
+ }
+ }
+ }
+ lines.each {line -> serviceEntries[path].append(new ByteArrayInputStream(line.getBytes()))}
+ }
+
+ @Override
+ boolean hasTransformedResource() {
+ return serviceEntries.size() > 0
+ }
+
+ @Override
+ void modifyOutputStream(ZipOutputStream os) {
+ serviceEntries.each { String path, ServiceStream stream ->
+ os.putNextEntry(new ZipEntry(path))
+ IOUtil.copy(stream.toInputStream(), os)
+ os.closeEntry()
+ }
+ }
+
+ static class ServiceStream extends ByteArrayOutputStream {
+
+ public ServiceStream(){
+ super( 1024 )
+ }
+
+ public void append( InputStream is ) throws IOException {
+ if ( count > 0 && buf[count - 1] != '\n' && buf[count - 1] != '\r' ) {
+ byte[] newline = '\n'.bytes
+ write(newline, 0, newline.length)
+ }
+ IOUtil.copy(is, this)
+ }
+
+ public InputStream toInputStream() {
+ return new ByteArrayInputStream( buf, 0, count )
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ ServiceFileTransformer include(String... includes) {
+ patternSet.include(includes)
+ return this
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ ServiceFileTransformer include(Iterable<String> includes) {
+ patternSet.include(includes)
+ return this
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ ServiceFileTransformer include(Spec<FileTreeElement> includeSpec) {
+ patternSet.include(includeSpec)
+ return this
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ ServiceFileTransformer include(Closure includeSpec) {
+ patternSet.include(includeSpec)
+ return this
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ ServiceFileTransformer exclude(String... excludes) {
+ patternSet.exclude(excludes)
+ return this
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ ServiceFileTransformer exclude(Iterable<String> excludes) {
+ patternSet.exclude(excludes)
+ return this
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ ServiceFileTransformer exclude(Spec<FileTreeElement> excludeSpec) {
+ patternSet.exclude(excludeSpec)
+ return this
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ ServiceFileTransformer exclude(Closure excludeSpec) {
+ patternSet.exclude(excludeSpec)
+ return this
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ Set<String> getIncludes() {
+ return patternSet.includes
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ ServiceFileTransformer setIncludes(Iterable<String> includes) {
+ patternSet.includes = includes
+ return this
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ Set<String> getExcludes() {
+ return patternSet.excludes
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ ServiceFileTransformer setExcludes(Iterable<String> excludes) {
+ patternSet.excludes = excludes
+ return this
+ }
+
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/Transformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/Transformer.groovy
new file mode 100644
index 0000000..b81ab83
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/Transformer.groovy
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.transformers
+
+import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator
+import org.apache.tools.zip.ZipOutputStream
+import org.gradle.api.file.FileTreeElement
+
+/**
+ * Modified from org.apache.maven.plugins.shade.resource.ResourceTransformer.java
+ * Original
+ * @author Jason van Zyl
+ *
+ * Modifications
+ * @author Charlie Knudsen
+ * @author John Engelman
+ */
+interface Transformer {
+
+ boolean canTransformResource(FileTreeElement element)
+
+ void transform(String path, InputStream is, List<Relocator> relocators)
+
+ boolean hasTransformedResource()
+
+ void modifyOutputStream(ZipOutputStream jos)
+}
diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/XmlAppendingTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/XmlAppendingTransformer.groovy
new file mode 100644
index 0000000..074e747
--- /dev/null
+++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/XmlAppendingTransformer.groovy
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.transformers
+
+import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator
+import org.apache.tools.zip.ZipEntry
+import org.apache.tools.zip.ZipOutputStream
+import org.gradle.api.file.FileTreeElement
+import org.jdom2.Attribute
+import org.jdom2.Content
+import org.jdom2.Document
+import org.jdom2.Element
+import org.jdom2.JDOMException
+import org.jdom2.input.SAXBuilder
+import org.jdom2.output.Format
+import org.jdom2.output.XMLOutputter
+import org.xml.sax.EntityResolver
+import org.xml.sax.InputSource
+import org.xml.sax.SAXException
+
+/**
+ * Appends multiple occurrences of some XML file.
+ *
+ * Modified from org.apache.maven.plugins.shade.resource.XmlAppendingTransformer.java
+ *
+ * Modifications
+ * @author John Engelman
+ */
+class XmlAppendingTransformer implements Transformer {
+ static final String XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
+
+ boolean ignoreDtd = true
+
+ String resource
+
+ Document doc
+
+ boolean canTransformResource(FileTreeElement element) {
+ def path = element.relativePath.pathString
+ if (resource != null && resource.equalsIgnoreCase(path)) {
+ return true
+ }
+
+ return false
+ }
+
+ void transform(String path, InputStream is, List<Relocator> relocators) {
+ Document r
+ try {
+ SAXBuilder builder = new SAXBuilder(false)
+ builder.setExpandEntities(false)
+ if (ignoreDtd) {
+ builder.setEntityResolver(new EntityResolver() {
+ InputSource resolveEntity(String publicId, String systemId)
+ throws SAXException, IOException {
+ return new InputSource(new StringReader(""))
+ }
+ })
+ }
+ r = builder.build(is)
+ }
+ catch (JDOMException e) {
+ throw new RuntimeException("Error processing resource " + resource + ": " + e.getMessage(), e)
+ }
+
+ if (doc == null) {
+ doc = r
+ } else {
+ Element root = r.getRootElement()
+
+ root.attributes.each { Attribute a ->
+
+ Element mergedEl = doc.getRootElement()
+ Attribute mergedAtt = mergedEl.getAttribute(a.getName(), a.getNamespace())
+ if (mergedAtt == null) {
+ mergedEl.setAttribute(a)
+ }
+ }
+
+ root.children.each { Content n ->
+ doc.getRootElement().addContent(n.clone())
+ }
+ }
+ }
+
+ boolean hasTransformedResource() {
+ return doc != null
+ }
+
+ void modifyOutputStream(ZipOutputStream os) {
+ os.putNextEntry(new ZipEntry(resource))
+ new XMLOutputter(Format.getPrettyFormat()).output(doc, os)
+
+ doc = null
+ }
+}
diff --git a/src/main/resources/META-INF/gradle-plugins/com.github.johnrengelman.shadow.properties b/src/main/resources/META-INF/gradle-plugins/com.github.johnrengelman.shadow.properties
new file mode 100644
index 0000000..e77803a
--- /dev/null
+++ b/src/main/resources/META-INF/gradle-plugins/com.github.johnrengelman.shadow.properties
@@ -0,0 +1,16 @@
+#
+# Copyright 2011 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.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.
+#
+implementation-class=com.github.jengelman.gradle.plugins.shadow.ShadowPlugin
diff --git a/src/main/resources/com/github/jengelman/gradle/plugins/shadow/internal/unixStartScript.txt b/src/main/resources/com/github/jengelman/gradle/plugins/shadow/internal/unixStartScript.txt
new file mode 100644
index 0000000..48ee799
--- /dev/null
+++ b/src/main/resources/com/github/jengelman/gradle/plugins/shadow/internal/unixStartScript.txt
@@ -0,0 +1,161 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## ${applicationName} start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and ${optsEnvironmentVar} to pass JVM options to this script.
+DEFAULT_JVM_OPTS=${defaultJvmOpts}
+
+APP_NAME="${applicationName}"
+APP_BASE_NAME=`basename "\$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "\$*"
+}
+
+die ( ) {
+ echo
+ echo "\$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if \$cygwin ; then
+ [ -n "\$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "\$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: \$0 may be a link
+PRG="\$0"
+# Need this for relative symlinks.
+while [ -h "\$PRG" ] ; do
+ ls=`ls -ld "\$PRG"`
+ link=`expr "\$ls" : '.*-> \\(.*\\)\$'`
+ if expr "\$link" : '/.*' > /dev/null; then
+ PRG="\$link"
+ else
+ PRG=`dirname "\$PRG"`"/\$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"\$PRG\"`/${appHomeRelativePath}" >&-
+APP_HOME="`pwd -P`"
+cd "\$SAVED" >&-
+
+# Determine the Java command to use to start the JVM.
+if [ -n "\$JAVA_HOME" ] ; then
+ if [ -x "\$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="\$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="\$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "\$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: \$JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "\$cygwin" = "false" -a "\$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ \$? -eq 0 ] ; then
+ if [ "\$MAX_FD" = "maximum" -o "\$MAX_FD" = "max" ] ; then
+ MAX_FD="\$MAX_FD_LIMIT"
+ fi
+ ulimit -n \$MAX_FD
+ if [ \$? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: \$MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: \$MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if \$darwin; then
+ GRADLE_OPTS="\$GRADLE_OPTS \\"-Xdock:name=\$APP_NAME\\" \\"-Xdock:icon=\$APP_HOME/media/gradle.icns\\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if \$cygwin ; then
+ APP_HOME=`cygpath --path --mixed "\$APP_HOME"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in \$ROOTDIRSRAW ; do
+ ROOTDIRS="\$ROOTDIRS\$SEP\$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^(\$ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "\$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="\$OURCYGPATTERN|(\$GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "\$@" ; do
+ CHECK=`echo "\$arg"|egrep -c "\$OURCYGPATTERN" -`
+ CHECK2=`echo "\$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ \$CHECK -ne 0 ] && [ \$CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args\$i`=`cygpath --path --ignore --mixed "\$arg"`
+ else
+ eval `echo args\$i`="\"\$arg\""
+ fi
+ i=\$((i+1))
+ done
+ case \$i in
+ (0) set -- ;;
+ (1) set -- "\$args0" ;;
+ (2) set -- "\$args0" "\$args1" ;;
+ (3) set -- "\$args0" "\$args1" "\$args2" ;;
+ (4) set -- "\$args0" "\$args1" "\$args2" "\$args3" ;;
+ (5) set -- "\$args0" "\$args1" "\$args2" "\$args3" "\$args4" ;;
+ (6) set -- "\$args0" "\$args1" "\$args2" "\$args3" "\$args4" "\$args5" ;;
+ (7) set -- "\$args0" "\$args1" "\$args2" "\$args3" "\$args4" "\$args5" "\$args6" ;;
+ (8) set -- "\$args0" "\$args1" "\$args2" "\$args3" "\$args4" "\$args5" "\$args6" "\$args7" ;;
+ (9) set -- "\$args0" "\$args1" "\$args2" "\$args3" "\$args4" "\$args5" "\$args6" "\$args7" "\$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And ${optsEnvironmentVar} values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("\$@")
+}
+eval splitJvmOpts \$DEFAULT_JVM_OPTS \$JAVA_OPTS \$${optsEnvironmentVar}
+<% if ( appNameSystemProperty ) { %>JVM_OPTS[\${#JVM_OPTS[*]}]="-D${appNameSystemProperty}=\$APP_BASE_NAME"<% } %>
+
+exec "\$JAVACMD" "\${JVM_OPTS[@]}" -jar ${mainApplicationJar} "\$@"
\ No newline at end of file
diff --git a/src/main/resources/com/github/jengelman/gradle/plugins/shadow/internal/windowsStartScript.txt b/src/main/resources/com/github/jengelman/gradle/plugins/shadow/internal/windowsStartScript.txt
new file mode 100644
index 0000000..c5c3ad8
--- /dev/null
+++ b/src/main/resources/com/github/jengelman/gradle/plugins/shadow/internal/windowsStartScript.txt
@@ -0,0 +1,89 @@
+ at if "%DEBUG%" == "" @echo off
+ at rem ##########################################################################
+ at rem
+ at rem ${applicationName} startup script for Windows
+ at rem
+ at rem ##########################################################################
+
+ at rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+ at rem Add default JVM options here. You can also use JAVA_OPTS and ${optsEnvironmentVar} to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=${defaultJvmOpts}
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.\
+
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%${appHomeRelativePath}
+
+ at rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+ at rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+ at rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+ at rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%\$
+
+:execute
+ at rem Setup the command line
+
+ at rem Execute ${applicationName}
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %${optsEnvironmentVar}% <% if ( appNameSystemProperty ) { %>"-D${appNameSystemProperty}=%APP_BASE_NAME%"<% } %> -jar "${mainApplicationJar}" %CMD_LINE_ARGS%
+
+:end
+ at rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable ${exitEnvironmentVar} if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%${exitEnvironmentVar}%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
\ No newline at end of file
diff --git a/src/main/resources/shadow-version.txt b/src/main/resources/shadow-version.txt
new file mode 100644
index 0000000..e2cac26
--- /dev/null
+++ b/src/main/resources/shadow-version.txt
@@ -0,0 +1 @@
+1.2.3
\ No newline at end of file
diff --git a/src/main/resources/shadowBanner.txt b/src/main/resources/shadowBanner.txt
new file mode 100644
index 0000000..1d231e8
--- /dev/null
+++ b/src/main/resources/shadowBanner.txt
@@ -0,0 +1,34 @@
+
+ .
+ .MMMMMO .M
+ .MMMMMMMMM. MMMM.
+ .MMMMMMMMMMMMMMM.
+ .MMMMMMMMMMMMM
+ .MMMMMMMMMMMM
+ .+MMMMMMMMM,ZMMM.
+ ...7MM8D8MM.ZMMMMM.
+ .. MMZ..MZZNMMMMM
+ .... MMMMMMMZZZ.MMMMMMOOOOOO..
+ ... 7MMMMMMMMMZZZMIMMMMOOOOOMMMM..
+ .. .~. .MMMMMOMZZZMZMMOOOOOMMMM MM.
+ .MMMMM ..MMM.7DOMOMOOOOOOOMM MMMMM Z
+ .. MMMMMMM.. . ...MMMMMMMMMOOOOOOMMMMMMMMMM
+ . .MMMMMMM. .MMMMM MMMMMOMOMMMMMMMMMMM
+ MMMMMMMMM .MMM.MMMMMMMMMMMOMMMMMMMMMMM
+ .MMMMMMMM $MMMM MMMMMMMMMMMMMMMMMM MMM
+ MMMMMMNMMMMMMMM M.MMMMMM.MMMMMMMM MMMMMM
+ ..MMMMMMMMMMMMMMMMMMMMMMMMMMMM.MNMMMMMMM .
+ ...MMMMMMMMMMM MMMMMMMMMMMMM.MMMMMMMM.
+ MMMMMMMMMM.MMMMMMMMMMMMMDMMMMMMMM.
+ ..MMMMMMMMMMMMMMMMMMM M,MMMMMMMMMMMMMMMZMMMMM +D ,
+ .:DMM.M. MMMMMMM.MMMMMMMMMMMMMMI:MMMMM :MMO
+ . MMMMMMMMMMMMMMMMMMMM.MMMMM8 NMMMN
+ ..MMMMMMMMMMMMMMMMMMMMM MMMMN.
+ .MMMMMMMMMMMMMMMM. MMM7 , . =.
+ MMMMMMMMMMMM.$MM M . MM7
+ MMMMMMMMM=MI:M8 . MNOM M
+ MMMMMMMMMM. .
+ MMMMMM .
+ +MM
+
+http://2.bp.blogspot.com/-urTvlwNjLeo/UGg5z9lxw5I/AAAAAAAAHRM/RCbSBi4I60s/s1600/The_Shadow_Knows_by_E_Mann.jpeg
\ No newline at end of file
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ApplicationSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ApplicationSpec.groovy
new file mode 100644
index 0000000..57aa47c
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ApplicationSpec.groovy
@@ -0,0 +1,194 @@
+package com.github.jengelman.gradle.plugins.shadow
+
+import com.github.jengelman.gradle.plugins.shadow.util.AppendableMavenFileRepository
+import com.github.jengelman.gradle.plugins.shadow.util.PluginSpecification
+import org.apache.tools.zip.ZipFile
+import org.gradle.testkit.runner.BuildResult
+import spock.lang.Issue
+
+import java.util.jar.Attributes
+import java.util.jar.JarFile
+
+class ApplicationSpec extends PluginSpecification {
+
+ AppendableMavenFileRepository repo
+ AppendableMavenFileRepository publishingRepo
+
+ def setup() {
+ repo = repo()
+ publishingRepo = repo('remote_repo')
+ }
+
+ def 'integration with application plugin'() {
+ given:
+ repo.module('shadow', 'a', '1.0')
+ .insertFile('a.properties', 'a')
+ .insertFile('a2.properties', 'a2')
+ .publish()
+
+ file('src/main/java/myapp/Main.java') << """
+ |package myapp;
+ |public class Main {
+ | public static void main(String[] args) {
+ | System.out.println("TestApp: Hello World! (" + args[0] + ")");
+ | }
+ |}
+ """.stripMargin()
+
+ buildFile << """
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |apply plugin: 'application'
+ |apply plugin: 'java'
+ |
+ |mainClassName = 'myapp.Main'
+ |
+ |version = '1.0'
+ |
+ |repositories {
+ | maven { url "${repo.uri}" }
+ |}
+ |
+ |dependencies {
+ | compile 'shadow:a:1.0'
+ |}
+ |
+ |runShadow {
+ | args 'foo'
+ |}
+ """.stripMargin()
+
+ settingsFile << "rootProject.name = 'myapp'"
+
+ when:
+ BuildResult result = runner.withArguments('runShadow').build()
+
+ then: 'tests that runShadow executed and exited'
+ assert result.output.contains('TestApp: Hello World! (foo)')
+
+ and: 'Check that the proper jar file was installed'
+ File installedJar = file('build/installShadow/myapp/lib/myapp-1.0-all.jar')
+ assert installedJar.exists()
+
+ and: 'And that jar file as the correct files in it'
+ contains(installedJar, ['a.properties', 'a2.properties', 'myapp/Main.class'])
+
+ and: 'Check the manifest attributes in the jar file are correct'
+ JarFile jar = new JarFile(installedJar)
+ Attributes attributes = jar.manifest.mainAttributes
+ assert attributes.getValue('Main-Class') == 'myapp.Main'
+
+ then: 'Check that the start scripts is written out and has the correct Java invocation'
+ File startScript = file('build/installShadow/myapp/bin/myapp')
+ assert startScript.exists()
+ assert startScript.text.contains("-jar \$APP_HOME/lib/myapp-1.0-all.jar")
+
+ cleanup:
+ jar?.close()
+ }
+
+ @Issue('SHADOW-89')
+ def 'shadow application distributions should use shadow jar'() {
+ given:
+ repo.module('shadow', 'a', '1.0')
+ .insertFile('a.properties', 'a')
+ .insertFile('a2.properties', 'a2')
+ .publish()
+
+ file('src/main/java/myapp/Main.java') << """
+ |package myapp;
+ |public class Main {
+ | public static void main(String[] args) {
+ | System.out.println("TestApp: Hello World! (" + args[0] + ")");
+ | }
+ |}
+ """.stripMargin()
+
+ buildFile << """
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |apply plugin: 'application'
+ |apply plugin: 'java'
+ |
+ |mainClassName = 'myapp.Main'
+ |
+ |version = '1.0'
+ |
+ |repositories {
+ | maven { url "${repo.uri}" }
+ |}
+ |
+ |dependencies {
+ | shadow 'shadow:a:1.0'
+ |}
+ |
+ |runShadow {
+ | args 'foo'
+ |}
+ """.stripMargin()
+
+ settingsFile << "rootProject.name = 'myapp'"
+
+ when:
+ runner.withArguments('distShadowZip').build()
+
+ then: 'Check that the distribution zip was created'
+ File zip = file('build/distributions/myapp-1.0.zip')
+ assert zip.exists()
+
+ and: 'Check that the zip contains the correct library files & scripts'
+ ZipFile zipFile = new ZipFile(zip)
+ assert zipFile.entries.find { it.name == 'myapp-1.0/lib/myapp-1.0-all.jar' }
+ assert zipFile.entries.find { it.name == 'myapp-1.0/lib/a-1.0.jar'}
+
+ cleanup:
+ zipFile?.close()
+ }
+
+ @Issue('SHADOW-90')
+ def 'installShadow does not execute dependent shadow task'() {
+ given:
+ repo.module('shadow', 'a', '1.0')
+ .insertFile('a.properties', 'a')
+ .insertFile('a2.properties', 'a2')
+ .publish()
+
+ file('src/main/java/myapp/Main.java') << """
+ |package myapp;
+ |public class Main {
+ | public static void main(String[] args) {
+ | System.out.println("TestApp: Hello World! (" + args[0] + ")");
+ | }
+ |}
+ """.stripMargin()
+
+ buildFile << """
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |apply plugin: 'application'
+ |apply plugin: 'java'
+ |
+ |mainClassName = 'myapp.Main'
+ |
+ |version = '1.0'
+ |
+ |repositories {
+ | maven { url "${repo.uri}" }
+ |}
+ |
+ |dependencies {
+ | compile 'shadow:a:1.0'
+ |}
+ |
+ |runShadow {
+ | args 'foo'
+ |}
+ """.stripMargin()
+
+ settingsFile << "rootProject.name = 'myapp'"
+
+ when:
+ runner.withArguments('installShadow').build()
+
+ then: 'Check that the proper jar file was installed'
+ File installedJar = file('build/installShadow/myapp/lib/myapp-1.0-all.jar')
+ assert installedJar.exists()
+ }
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/FilteringSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/FilteringSpec.groovy
new file mode 100644
index 0000000..b1c6224
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/FilteringSpec.groovy
@@ -0,0 +1,441 @@
+package com.github.jengelman.gradle.plugins.shadow
+
+import com.github.jengelman.gradle.plugins.shadow.util.PluginSpecification
+import org.gradle.testkit.runner.BuildResult
+import org.gradle.testkit.runner.TaskOutcome
+import spock.lang.Ignore
+import spock.lang.IgnoreRest
+import spock.lang.Issue
+
+class FilteringSpec extends PluginSpecification {
+
+ def setup() {
+ repo.module('shadow', 'a', '1.0')
+ .insertFile('a.properties', 'a')
+ .insertFile('a2.properties', 'a2')
+ .publish()
+ repo.module('shadow', 'b', '1.0')
+ .insertFile('b.properties', 'b')
+ .publish()
+
+ buildFile << """
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |apply plugin: 'java'
+ |
+ |repositories { maven { url "${repo.uri}" } }
+ |dependencies {
+ | compile 'shadow:a:1.0'
+ | compile 'shadow:b:1.0'
+ |}
+ |
+ |shadowJar {
+ | baseName = 'shadow'
+ | classifier = null
+ |}
+ """.stripMargin()
+
+ }
+
+ def 'include all dependencies'() {
+ when:
+ runner.withArguments('shadowJar').build()
+
+ then:
+ contains(output, ['a.properties', 'a2.properties', 'b.properties'])
+ }
+
+ def 'exclude files'() {
+ given:
+ buildFile << """
+ |shadowJar {
+ | exclude 'a2.properties'
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadowJar').build()
+
+ then:
+ contains(output, ['a.properties', 'b.properties'])
+
+ and:
+ doesNotContain(output, ['a2.properties'])
+ }
+
+ def "exclude dependency"() {
+ given:
+ repo.module('shadow', 'c', '1.0')
+ .insertFile('c.properties', 'c')
+ .publish()
+ repo.module('shadow', 'd', '1.0')
+ .insertFile('d.properties', 'd')
+ .dependsOn('c')
+ .publish()
+
+ buildFile << '''
+ |dependencies {
+ | compile 'shadow:d:1.0'
+ |}
+ |
+ |shadowJar {
+ | dependencies {
+ | exclude(dependency('shadow:d:1.0'))
+ | }
+ |}
+ '''.stripMargin()
+
+ when:
+ runner.withArguments('shadowJar').build()
+
+ then:
+ contains(output, ['a.properties', 'a2.properties', 'b.properties', 'c.properties'])
+
+ and:
+ doesNotContain(output, ['d.properties'])
+ }
+
+ @Issue('SHADOW-83')
+ def "exclude dependency using wildcard syntax"() {
+ given:
+ repo.module('shadow', 'c', '1.0')
+ .insertFile('c.properties', 'c')
+ .publish()
+ repo.module('shadow', 'd', '1.0')
+ .insertFile('d.properties', 'd')
+ .dependsOn('c')
+ .publish()
+
+ buildFile << '''
+ |dependencies {
+ | compile 'shadow:d:1.0'
+ |}
+ |
+ |shadowJar {
+ | dependencies {
+ | exclude(dependency('shadow:d:.*'))
+ | }
+ |}
+ '''.stripMargin()
+
+ when:
+ runner.withArguments('shadowJar').build()
+
+ then:
+ contains(output, ['a.properties', 'a2.properties', 'b.properties', 'c.properties'])
+
+ and:
+ doesNotContain(output, ['d.properties'])
+ }
+
+ @Issue("SHADOW-54")
+ @Ignore("TODO - need to figure out the test pollution here")
+ def "dependency exclusions affect UP-TO-DATE check"() {
+ given:
+ repo.module('shadow', 'c', '1.0')
+ .insertFile('c.properties', 'c')
+ .publish()
+ repo.module('shadow', 'd', '1.0')
+ .insertFile('d.properties', 'd')
+ .dependsOn('c')
+ .publish()
+
+ buildFile << '''
+ |dependencies {
+ | compile 'shadow:d:1.0'
+ |}
+ |
+ |shadowJar {
+ | dependencies {
+ | exclude(dependency('shadow:d:1.0'))
+ | }
+ |}
+ '''.stripMargin()
+
+ when:
+ runner.withArguments('shadowJar').build()
+
+ then:
+ contains(output, ['a.properties', 'a2.properties', 'b.properties', 'c.properties'])
+
+ and:
+ doesNotContain(output, ['d.properties'])
+
+ when: 'Update build file shadowJar dependency exclusion'
+ buildFile.text = buildFile.text.replace('exclude(dependency(\'shadow:d:1.0\'))',
+ 'exclude(dependency(\'shadow:c:1.0\'))')
+
+ BuildResult result = runner.withArguments('shadowJar').build()
+
+ then:
+ assert result.task(':shadowJar').outcome == TaskOutcome.SUCCESS
+
+ and:
+ contains(output, ['a.properties', 'a2.properties', 'b.properties', 'd.properties'])
+
+ and:
+ doesNotContain(output, ['c.properties'])
+ }
+
+ @Issue("SHADOW-62")
+ @Ignore
+ def "project exclusions affect UP-TO-DATE check"() {
+ given:
+ repo.module('shadow', 'c', '1.0')
+ .insertFile('c.properties', 'c')
+ .publish()
+ repo.module('shadow', 'd', '1.0')
+ .insertFile('d.properties', 'd')
+ .dependsOn('c')
+ .publish()
+
+ buildFile << '''
+ |dependencies {
+ | compile 'shadow:d:1.0'
+ |}
+ |
+ |shadowJar {
+ | dependencies {
+ | exclude(dependency('shadow:d:1.0'))
+ | }
+ |}
+ '''.stripMargin()
+
+ when:
+ runner.withArguments('shadowJar').build()
+
+ then:
+ contains(output, ['a.properties', 'a2.properties', 'b.properties', 'c.properties'])
+
+ and:
+ doesNotContain(output, ['d.properties'])
+
+ when: 'Update build file shadowJar dependency exclusion'
+ buildFile.text << '''
+ |shadowJar {
+ | exclude 'a.properties'
+ |}
+ '''.stripMargin()
+
+ BuildResult result = runner.withArguments('shadowJar').build()
+
+ then:
+ assert result.task(':shadowJar').outcome == TaskOutcome.SUCCESS
+
+ and:
+ contains(output, ['a2.properties', 'b.properties', 'd.properties'])
+
+ and:
+ doesNotContain(output, ['a.properties', 'c.properties'])
+ }
+
+ def "include dependency, excluding all others"() {
+ given:
+ repo.module('shadow', 'c', '1.0')
+ .insertFile('c.properties', 'c')
+ .publish()
+ repo.module('shadow', 'd', '1.0')
+ .insertFile('d.properties', 'd')
+ .dependsOn('c')
+ .publish()
+
+ file('src/main/java/shadow/Passed.java') << '''
+ |package shadow;
+ |public class Passed {}
+ '''.stripMargin()
+
+ buildFile << '''
+ |dependencies {
+ | compile 'shadow:d:1.0'
+ |}
+ |
+ |shadowJar {
+ | dependencies {
+ | include(dependency('shadow:d:1.0'))
+ | }
+ |}
+ '''.stripMargin()
+
+ when:
+ runner.withArguments('shadowJar').build()
+
+ then:
+ contains(output, ['d.properties', 'shadow/Passed.class'])
+
+ and:
+ doesNotContain(output, ['a.properties', 'a2.properties', 'b.properties', 'c.properties'])
+ }
+
+ def 'filter project dependencies'() {
+ given:
+ buildFile.text = ''
+
+ file('settings.gradle') << """
+ |include 'client', 'server'
+ """.stripMargin()
+
+ file('client/src/main/java/client/Client.java') << """
+ |package client;
+ |public class Client {}
+ """.stripMargin()
+
+ file('client/build.gradle') << """
+ |${defaultBuildScript}
+ |apply plugin: 'java'
+ |repositories { maven { url "${repo.uri}" } }
+ |dependencies { compile 'junit:junit:3.8.2' }
+ """.stripMargin()
+
+ file('server/src/main/java/server/Server.java') << """
+ |package server;
+ |import client.Client;
+ |public class Server {}
+ """.stripMargin()
+
+ file('server/build.gradle') << """
+ |${defaultBuildScript}
+ |apply plugin: 'java'
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |
+ |repositories { maven { url "${repo.uri}" } }
+ |dependencies { compile project(':client') }
+ |
+ |shadowJar {
+ | baseName = 'shadow'
+ | classifier = null
+ | dependencies {
+ | exclude(project(':client'))
+ | }
+ |}
+ """.stripMargin()
+
+ File serverOutput = file('server/build/libs/shadow.jar')
+
+ when:
+ runner.withArguments(':server:shadowJar').build()
+
+ then:
+ doesNotContain(serverOutput, [
+ 'client/Client.class',
+ ])
+
+ and:
+ contains(serverOutput, ['server/Server.class', 'junit/framework/Test.class'])
+ }
+
+ def 'exclude a transitive project dependency'() {
+ given:
+ buildFile.text = ''
+
+ file('settings.gradle') << """
+ |include 'client', 'server'
+ """.stripMargin()
+
+ file('client/src/main/java/client/Client.java') << """
+ |package client;
+ |public class Client {}
+ """.stripMargin()
+
+ file('client/build.gradle') << """
+ |${defaultBuildScript}
+ |apply plugin: 'java'
+ |repositories { maven { url "${repo.uri}" } }
+ |dependencies { compile 'junit:junit:3.8.2' }
+ """.stripMargin()
+
+ file('server/src/main/java/server/Server.java') << """
+ |package server;
+ |import client.Client;
+ |public class Server {}
+ """.stripMargin()
+
+ file('server/build.gradle') << """
+ |${defaultBuildScript}
+ |apply plugin: 'java'
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |
+ |repositories { maven { url "${repo.uri}" } }
+ |dependencies { compile project(':client') }
+ |
+ |shadowJar {
+ | baseName = 'shadow'
+ | classifier = null
+ | dependencies {
+ | exclude(dependency {
+ | it.moduleGroup == 'junit'
+ | })
+ | }
+ |}
+ """.stripMargin()
+
+ File serverOutput = file('server/build/libs/shadow.jar')
+
+ when:
+ runner.withArguments(':server:shadowJar').build()
+
+ then:
+ doesNotContain(serverOutput, [
+ 'junit/framework/Test.class'
+ ])
+
+ and:
+ contains(serverOutput, [
+ 'client/Client.class',
+ 'server/Server.class'])
+ }
+
+ //http://mail-archives.apache.org/mod_mbox/ant-user/200506.mbox/%3C001d01c57756$6dc35da0$dc00a8c0@CTEGDOMAIN.COM%3E
+ def 'verify exclude precedence over include'() {
+ given:
+ buildFile << """
+ |shadowJar {
+ | include '*.jar'
+ | include '*.properties'
+ | exclude 'a2.properties'
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadowJar').build()
+
+ then:
+ contains(output, ['a.properties', 'b.properties'])
+
+ and:
+ doesNotContain(output, ['a2.properties'])
+ }
+
+ @Issue("SHADOW-69")
+ def "handle exclude with circular dependency"() {
+ given:
+ repo.module('shadow', 'c', '1.0')
+ .insertFile('c.properties', 'c')
+ .dependsOn('d')
+ .publish()
+ repo.module('shadow', 'd', '1.0')
+ .insertFile('d.properties', 'd')
+ .dependsOn('c')
+ .publish()
+
+ buildFile << '''
+ |dependencies {
+ | compile 'shadow:d:1.0'
+ |}
+ |
+ |shadowJar {
+ | dependencies {
+ | exclude(dependency('shadow:d:1.0'))
+ | }
+ |}
+ '''.stripMargin()
+
+ when:
+ runner.withArguments('shadowJar').build()
+
+ then:
+ contains(output, ['a.properties', 'a2.properties', 'b.properties', 'c.properties'])
+
+ and:
+ doesNotContain(output, ['d.properties'])
+ }
+
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/PublishingSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/PublishingSpec.groovy
new file mode 100644
index 0000000..ed65517
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/PublishingSpec.groovy
@@ -0,0 +1,147 @@
+package com.github.jengelman.gradle.plugins.shadow
+
+import com.github.jengelman.gradle.plugins.shadow.util.AppendableMavenFileRepository
+import com.github.jengelman.gradle.plugins.shadow.util.PluginSpecification
+
+class PublishingSpec extends PluginSpecification {
+
+ AppendableMavenFileRepository repo
+ AppendableMavenFileRepository publishingRepo
+
+ def setup() {
+ repo = repo()
+ publishingRepo = repo('remote_repo')
+ }
+
+ def "publish shadow jar with maven plugin"() {
+ given:
+ repo.module('shadow', 'a', '1.0')
+ .insertFile('a.properties', 'a')
+ .insertFile('a2.properties', 'a2')
+ .publish()
+ repo.module('shadow', 'b', '1.0')
+ .insertFile('b.properties', 'b')
+ .publish()
+
+ settingsFile << "rootProject.name = 'maven'"
+ buildFile << """
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |apply plugin: 'maven'
+ |apply plugin: 'java'
+ |
+ |group = 'shadow'
+ |version = '1.0'
+ |
+ |repositories { maven { url "${repo.uri}" } }
+ |dependencies {
+ | compile 'shadow:a:1.0'
+ | shadow 'shadow:b:1.0'
+ |}
+ |
+ |shadowJar {
+ | baseName = 'maven-all'
+ | classifier = null
+ |}
+ |
+ |uploadShadow {
+ | repositories {
+ | mavenDeployer {
+ | repository(url: "${publishingRepo.uri}")
+ | }
+ | }
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('uploadShadow').build()
+
+ then: 'Check that shadow artifact exists'
+ File publishedFile = publishingRepo.rootDir.file('shadow/maven-all/1.0/maven-all-1.0.jar').canonicalFile
+ assert publishedFile.exists()
+
+ and: 'Check contents of shadow artifact'
+ contains(publishedFile, ['a.properties', 'a2.properties'])
+
+ and: 'Check that shadow artifact pom exists and contents'
+ File pom = publishingRepo.rootDir.file('shadow/maven-all/1.0/maven-all-1.0.pom').canonicalFile
+ assert pom.exists()
+
+ def contents = new XmlSlurper().parse(pom)
+ assert contents.dependencies.size() == 1
+ assert contents.dependencies[0].dependency.size() == 1
+
+ def dependency = contents.dependencies[0].dependency[0]
+ assert dependency.groupId.text() == 'shadow'
+ assert dependency.artifactId.text() == 'b'
+ assert dependency.version.text() == '1.0'
+ }
+
+ def "publish shadow jar with maven-publish plugin"() {
+ given:
+ repo.module('shadow', 'a', '1.0')
+ .insertFile('a.properties', 'a')
+ .insertFile('a2.properties', 'a2')
+ .publish()
+ repo.module('shadow', 'b', '1.0')
+ .insertFile('b.properties', 'b')
+ .publish()
+
+ settingsFile << "rootProject.name = 'maven'"
+ buildFile << """
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |apply plugin: 'maven-publish'
+ |apply plugin: 'java'
+ |
+ |group = 'shadow'
+ |version = '1.0'
+ |
+ |repositories { maven { url "${repo.uri}" } }
+ |dependencies {
+ | compile 'shadow:a:1.0'
+ | shadow 'shadow:b:1.0'
+ |}
+ |
+ |shadowJar {
+ | classifier = ''
+ | baseName = 'maven-all'
+ |}
+ |
+ |publishing {
+ | publications {
+ | shadow(MavenPublication) {
+ | from components.shadow
+ | artifactId = 'maven-all'
+ | }
+ | }
+ | repositories {
+ | maven {
+ | url "${publishingRepo.uri}"
+ | }
+ | }
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('publish').build()
+
+ then:
+ File publishedFile = publishingRepo.rootDir.file('shadow/maven-all/1.0/maven-all-1.0.jar').canonicalFile
+ assert publishedFile.exists()
+
+ and:
+ contains(publishedFile, ['a.properties', 'a2.properties'])
+
+ and:
+ File pom = publishingRepo.rootDir.file('shadow/maven-all/1.0/maven-all-1.0.pom').canonicalFile
+ assert pom.exists()
+
+ def contents = new XmlSlurper().parse(pom)
+ assert contents.dependencies.size() == 1
+ assert contents.dependencies[0].dependency.size() == 1
+
+ def dependency = contents.dependencies[0].dependency[0]
+ assert dependency.groupId.text() == 'shadow'
+ assert dependency.artifactId.text() == 'b'
+ assert dependency.version.text() == '1.0'
+ }
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/RelocationSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/RelocationSpec.groovy
new file mode 100644
index 0000000..7fc69ac
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/RelocationSpec.groovy
@@ -0,0 +1,315 @@
+package com.github.jengelman.gradle.plugins.shadow
+
+import com.github.jengelman.gradle.plugins.shadow.util.PluginSpecification
+import spock.lang.Issue
+
+import java.util.jar.Attributes
+import java.util.jar.JarFile
+
+class RelocationSpec extends PluginSpecification {
+
+ @Issue('SHADOW-58')
+ def "relocate dependency files"() {
+ given:
+ buildFile << """
+ |apply plugin: 'java'
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |
+ |repositories { maven { url "${repo.uri}" } }
+ |
+ |dependencies {
+ | compile 'junit:junit:3.8.2'
+ |}
+ |
+ |shadowJar {
+ | baseName = 'shadow'
+ | classifier = null
+ | relocate 'junit.textui', 'a'
+ | relocate 'junit.framework', 'b'
+ | manifest {
+ | attributes 'TEST-VALUE': 'FOO'
+ | }
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadowJar').build()
+
+ then:
+ contains(output, [
+ 'META-INF/MANIFEST.MF',
+ 'a/ResultPrinter.class',
+ 'a/TestRunner.class',
+ 'b/Assert.class',
+ 'b/AssertionFailedError.class',
+ 'b/ComparisonCompactor.class',
+ 'b/ComparisonFailure.class',
+ 'b/Protectable.class',
+ 'b/Test.class',
+ 'b/TestCase.class',
+ 'b/TestFailure.class',
+ 'b/TestListener.class',
+ 'b/TestResult$1.class',
+ 'b/TestResult.class',
+ 'b/TestSuite$1.class',
+ 'b/TestSuite.class'
+ ])
+
+ and:
+ doesNotContain(output, [
+ 'junit/textui/ResultPrinter.class',
+ 'junit/textui/TestRunner.class',
+ 'junit/framework/Assert.class',
+ 'junit/framework/AssertionFailedError.class',
+ 'junit/framework/ComparisonCompactor.class',
+ 'junit/framework/ComparisonFailure.class',
+ 'junit/framework/Protectable.class',
+ 'junit/framework/Test.class',
+ 'junit/framework/TestCase.class',
+ 'junit/framework/TestFailure.class',
+ 'junit/framework/TestListener.class',
+ 'junit/framework/TestResult$1.class',
+ 'junit/framework/TestResult.class',
+ 'junit/framework/TestSuite$1.class',
+ 'junit/framework/TestSuite.class'
+ ])
+
+ and: 'Test that manifest file exists with contents'
+ JarFile jar = new JarFile(output)
+ Attributes attributes = jar.manifest.getMainAttributes()
+ String val = attributes.getValue('TEST-VALUE')
+ assert val == 'FOO'
+ }
+
+ def "relocate dependency files with filtering"() {
+ given:
+ buildFile << """
+ |apply plugin: 'java'
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |
+ |repositories { maven { url "${repo.uri}" } }
+ |
+ |dependencies {
+ | compile 'junit:junit:3.8.2'
+ |}
+ |
+ |shadowJar {
+ | baseName = 'shadow'
+ | classifier = null
+ | relocate('junit.textui', 'a') {
+ | exclude 'junit.textui.TestRunner'
+ | }
+ | relocate('junit.framework', 'b') {
+ | include 'junit.framework.Test*'
+ | }
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadowJar').build()
+
+ then:
+ contains(output, [
+ 'a/ResultPrinter.class',
+ 'b/Test.class',
+ 'b/TestCase.class',
+ 'b/TestFailure.class',
+ 'b/TestListener.class',
+ 'b/TestResult$1.class',
+ 'b/TestResult.class',
+ 'b/TestSuite$1.class',
+ 'b/TestSuite.class'
+ ])
+
+ and:
+ doesNotContain(output, [
+ 'a/TestRunner.class',
+ 'b/Assert.class',
+ 'b/AssertionFailedError.class',
+ 'b/ComparisonCompactor.class',
+ 'b/ComparisonFailure.class',
+ 'b/Protectable.class'
+ ])
+
+ and:
+ contains(output, [
+ 'junit/textui/TestRunner.class',
+ 'junit/framework/Assert.class',
+ 'junit/framework/AssertionFailedError.class',
+ 'junit/framework/ComparisonCompactor.class',
+ 'junit/framework/ComparisonFailure.class',
+ 'junit/framework/Protectable.class'
+ ])
+ }
+
+ @Issue(['SHADOW-55', 'SHADOW-53'])
+ def "remap class names for relocated files in project source"() {
+ given:
+ buildFile << """
+ |apply plugin: 'java'
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |
+ |repositories { maven { url "${repo.uri}" } }
+ |
+ |dependencies {
+ | compile 'junit:junit:3.8.2'
+ |}
+ |
+ |shadowJar {
+ | baseName = 'shadow'
+ | classifier = null
+ | relocate 'junit.framework', 'shadow.junit'
+ |}
+ """.stripMargin()
+
+ file('src/main/java/shadow/ShadowTest.java') << '''
+ |package shadow;
+ |
+ |import junit.framework.Test;
+ |import junit.framework.TestResult;
+ |public class ShadowTest implements Test {
+ | public int countTestCases() { return 0; }
+ | public void run(TestResult result) { }
+ |}
+ '''.stripMargin()
+
+ when:
+ runner.withArguments('shadowJar').build()
+
+ then:
+ contains(output, [
+ 'shadow/ShadowTest.class',
+ 'shadow/junit/Test.class',
+ 'shadow/junit'
+ ])
+
+ and:
+ doesNotContain(output, [
+ 'junit/framework',
+ 'junit/framework/Test.class'
+ ])
+
+ and: 'check that the class can be loaded. If the file was not relocated properly, we should get a NoDefClassFound'
+ // Isolated class loader with only the JVM system jars and the output jar from the test project
+ URLClassLoader classLoader = new URLClassLoader([output.toURI().toURL()] as URL[],
+ ClassLoader.systemClassLoader.parent)
+ classLoader.loadClass('shadow.ShadowTest')
+ }
+
+ @Issue('SHADOW-61')
+ def "relocate does not drop dependency resources"() {
+ given: 'Core project with dependency and resource'
+ file('core/build.gradle') << """
+ |apply plugin: 'java'
+ |
+ |repositories { maven { url "${repo.uri}" } }
+ |dependencies { compile 'junit:junit:3.8.2' }
+ """.stripMargin()
+
+ file('core/src/main/resources/TEST') << 'TEST RESOURCE'
+ file('core/src/main/resources/test.properties') << 'name=test'
+ file('core/src/main/java/core/Core.java') << '''
+ |package core;
+ |
+ |import junit.framework.Test;
+ |
+ |public class Core {}
+ '''.stripMargin()
+
+ and: 'App project with shadow, relocation, and project dependency'
+ file('app/build.gradle') << """
+ |apply plugin: 'java'
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |
+ |repositories { maven { url "${repo.uri}" } }
+ |dependencies { compile project(':core') }
+ |
+ |shadowJar {
+ | baseName = 'shadow'
+ | classifier = null
+ | relocate 'core', 'app.core'
+ | relocate 'junit.framework', 'app.junit.framework'
+ |}
+ """.stripMargin()
+
+ file('app/src/main/resources/APP-TEST') << 'APP TEST RESOURCE'
+ file('app/src/main/java/app/App.java') << '''
+ |package app;
+ |
+ |import core.Core;
+ |import junit.framework.Test;
+ |
+ |public class App {}
+ '''.stripMargin()
+
+ and: 'Configure multi-project build'
+ settingsFile << '''
+ |include 'core', 'app'
+ '''.stripMargin()
+
+ when:
+ runner.withArguments(':app:shadowJar').build()
+
+ then:
+ File appOutput = file('app/build/libs/shadow.jar')
+ assert appOutput.exists()
+
+ and:
+ contains(appOutput, [
+ 'TEST',
+ 'APP-TEST',
+ 'test.properties',
+ 'app/core/Core.class',
+ 'app/App.class',
+ 'app/junit/framework/Test.class'
+ ])
+ }
+
+ @Issue(['SHADOW-93', 'SHADOW-114'])
+ def "relocate resource files"() {
+ given:
+ repo.module('shadow', 'dep', '1.0')
+ .insertFile('foo/dep.properties', 'c')
+ .publish()
+ file('src/main/java/foo/Foo.java') << '''
+ |package foo;
+ |
+ |class Foo {}
+ |'''.stripMargin()
+ file('src/main/resources/foo/foo.properties') << 'name=foo'
+
+ buildFile << """
+ |apply plugin: 'java'
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |
+ |repositories { maven { url "${repo.uri}" } }
+ |
+ |dependencies {
+ | compile 'shadow:dep:1.0'
+ |}
+ |
+ |shadowJar {
+ | baseName = 'shadow'
+ | classifier = null
+ | relocate 'foo', 'bar'
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadowJar').build()
+
+ then:
+ contains(output, [
+ 'bar/Foo.class',
+ 'bar/foo.properties',
+ 'bar/dep.properties'
+ ])
+
+ and:
+ doesNotContain(output, [
+ 'foo/Foo.class',
+ 'foo/foo.properties',
+ 'foo/dep.properties'
+ ])
+ }
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy
new file mode 100644
index 0000000..41e260e
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy
@@ -0,0 +1,536 @@
+package com.github.jengelman.gradle.plugins.shadow
+
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
+import com.github.jengelman.gradle.plugins.shadow.util.AppendableMavenFileRepository
+import com.github.jengelman.gradle.plugins.shadow.util.PluginSpecification
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.plugins.JavaPlugin
+import org.gradle.testfixtures.ProjectBuilder
+import org.gradle.testkit.runner.GradleRunner
+import spock.lang.Issue
+import spock.lang.Unroll
+
+import java.util.jar.Attributes
+import java.util.jar.JarFile
+
+class ShadowPluginSpec extends PluginSpecification {
+
+ def 'apply plugin'() {
+ given:
+ String projectName = 'myshadow'
+ String version = '1.0.0'
+
+ Project project = ProjectBuilder.builder().withName(projectName).build()
+ project.version = version
+
+ when:
+ project.plugins.apply(ShadowPlugin)
+
+ then:
+ project.plugins.hasPlugin(ShadowPlugin)
+
+ and:
+ assert !project.tasks.findByName('shadowJar')
+
+ when:
+ project.plugins.apply(JavaPlugin)
+
+ then:
+ ShadowJar shadow = project.tasks.findByName('shadowJar')
+ assert shadow
+ assert shadow.baseName == projectName
+ assert shadow.destinationDir == new File(project.buildDir, 'libs')
+ assert shadow.version == version
+ assert shadow.classifier == 'all'
+ assert shadow.extension == 'jar'
+
+ and:
+ Configuration shadowConfig = project.configurations.findByName('shadow')
+ assert shadowConfig
+ shadowConfig.artifacts.file.contains(shadow.archivePath)
+
+ }
+
+ @Unroll
+ def 'apply plugin and run in Gradle #version'() {
+ given:
+ GradleRunner versionRunner = GradleRunner.create()
+ .withGradleVersion(version)
+ .withArguments('--stacktrace')
+ .withProjectDir(dir.root)
+ .forwardOutput()
+ .withDebug(true)
+ .withTestKitDir(getTestKitDir())
+
+
+ File one = buildJar('one.jar').insertFile('META-INF/services/shadow.Shadow',
+ 'one # NOTE: No newline terminates this line/file').write()
+
+ repo.module('shadow', 'two', '1.0').insertFile('META-INF/services/shadow.Shadow',
+ 'two # NOTE: No newline terminates this line/file').publish()
+
+ buildFile << """
+ |apply plugin: 'java'
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |
+ |repositories { maven { url "${repo.uri}" } }
+ |dependencies {
+ | compile 'junit:junit:3.8.2'
+ | compile files('${escapedPath(one)}')
+ |}
+ |
+ |shadowJar {
+ | baseName = 'shadow'
+ | classifier = null
+ | mergeServiceFiles()
+ |}
+ """.stripMargin()
+
+ when:
+ versionRunner.withArguments('shadowJar', '--stacktrace').build()
+
+ then:
+ assert output.exists()
+
+ where:
+ version << ['1.12', '2.0', '2.5', '2.11-rc-1']
+ }
+
+ def 'shadow copy'() {
+ given:
+ URL artifact = this.class.classLoader.getResource('test-artifact-1.0-SNAPSHOT.jar')
+ URL project = this.class.classLoader.getResource('test-project-1.0-SNAPSHOT.jar')
+
+ buildFile << """
+ |task shadow(type: ${ShadowJar.name}) {
+ | destinationDir = buildDir
+ | baseName = 'shadow'
+ | from('${artifact.path}')
+ | from('${project.path}')
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadow').build()
+
+ then:
+ File output = file('build/shadow.jar')
+ assert output.exists()
+ }
+
+ def 'include project sources'() {
+ given:
+ file('src/main/java/shadow/Passed.java') << '''
+ |package shadow;
+ |public class Passed {}
+ '''.stripMargin()
+
+ buildFile << """
+ |apply plugin: 'java'
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |repositories { maven { url "${repo.uri}" } }
+ |dependencies { compile 'junit:junit:3.8.2' }
+ |
+ |shadowJar {
+ | baseName = 'shadow'
+ | classifier = null
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadowJar').build()
+
+ then:
+ contains(output, ['shadow/Passed.class', 'junit/framework/Test.class'])
+
+ and:
+ doesNotContain(output, ['/'])
+ }
+
+ def 'include project dependencies'() {
+ given:
+ file('settings.gradle') << """
+ |include 'client', 'server'
+ """.stripMargin()
+
+ file('client/src/main/java/client/Client.java') << """
+ |package client;
+ |public class Client {}
+ |""".stripMargin()
+
+ file('client/build.gradle') << """
+ |apply plugin: 'java'
+ |repositories { maven { url "${repo.uri}" } }
+ |dependencies { compile 'junit:junit:3.8.2' }
+ """.stripMargin()
+
+ file('server/src/main/java/server/Server.java') << """
+ |package server;
+ |
+ |import client.Client;
+ |
+ |public class Server {}
+ """.stripMargin()
+
+ file('server/build.gradle') << """
+ |apply plugin: 'java'
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |
+ |repositories { maven { url "${repo.uri}" } }
+ |dependencies { compile project(':client') }
+ |
+ |shadowJar {
+ | baseName = 'shadow'
+ | classifier = null
+ |}
+ """.stripMargin()
+
+ File serverOutput = file('server/build/libs/shadow.jar')
+
+ when:
+ runner.withArguments(':server:shadowJar').build()
+
+ then:
+ contains(serverOutput, [
+ 'client/Client.class',
+ 'server/Server.class',
+ 'junit/framework/Test.class'
+ ])
+ }
+
+ def 'depend on project shadow jar'() {
+ given:
+ file('settings.gradle') << """
+ |include 'client', 'server'
+ """.stripMargin()
+
+ file('client/src/main/java/client/Client.java') << """
+ |package client;
+ |public class Client {}
+ |""".stripMargin()
+
+ file('client/build.gradle') << """
+ |apply plugin: 'java'
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |repositories { maven { url "${repo.uri}" } }
+ |dependencies { compile 'junit:junit:3.8.2' }
+ |
+ |shadowJar {
+ | relocate 'junit.framework', 'client.junit.framework'
+ |}
+ """.stripMargin()
+
+ file('server/src/main/java/server/Server.java') << """
+ |package server;
+ |
+ |import client.Client;
+ |import client.junit.framework.Test;
+ |
+ |public class Server {}
+ """.stripMargin()
+
+ file('server/build.gradle') << """
+ |apply plugin: 'java'
+ |
+ |repositories { maven { url "${repo.uri}" } }
+ |dependencies { compile project(path: ':client', configuration: 'shadow') }
+ """.stripMargin()
+
+ File serverOutput = file('server/build/libs/server.jar')
+
+ when:
+ runner.withArguments(':server:jar').build()
+
+ then:
+ contains(serverOutput, [
+ 'server/Server.class'
+ ])
+
+ and:
+ doesNotContain(serverOutput, [
+ 'client/Client.class',
+ 'junit/framework/Test.class',
+ 'client/junit/framework/Test.class'
+ ])
+ }
+
+ def 'shadow a project shadow jar'() {
+ given:
+ file('settings.gradle') << """
+ |include 'client', 'server'
+ """.stripMargin()
+
+ file('client/src/main/java/client/Client.java') << """
+ |package client;
+ |public class Client {}
+ |""".stripMargin()
+
+ file('client/build.gradle') << """
+ |apply plugin: 'java'
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |repositories { maven { url "${repo.uri}" } }
+ |dependencies { compile 'junit:junit:3.8.2' }
+ |
+ |shadowJar {
+ | relocate 'junit.framework', 'client.junit.framework'
+ |}
+ """.stripMargin()
+
+ file('server/src/main/java/server/Server.java') << """
+ |package server;
+ |
+ |import client.Client;
+ |import client.junit.framework.Test;
+ |
+ |public class Server {}
+ """.stripMargin()
+
+ file('server/build.gradle') << """
+ |apply plugin: 'java'
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |
+ |repositories { maven { url "${repo.uri}" } }
+ |dependencies { compile project(path: ':client', configuration: 'shadow') }
+ |
+ |shadowJar {
+ | baseName = 'shadow'
+ | classifier = null
+ |}
+ """.stripMargin()
+
+ File serverOutput = file('server/build/libs/shadow.jar')
+
+ when:
+ runner.withArguments(':server:shadowJar').build()
+
+ then:
+ contains(serverOutput, [
+ 'client/Client.class',
+ 'client/junit/framework/Test.class',
+ 'server/Server.class',
+ ])
+
+ and:
+ doesNotContain(serverOutput, [
+ 'junit/framework/Test.class'
+ ])
+ }
+
+ def "exclude INDEX.LIST, *.SF, *.DSA, and *.RSA by default"() {
+ given:
+ AppendableMavenFileRepository repo = repo()
+
+ repo.module('shadow', 'a', '1.0')
+ .insertFile('a.properties', 'a')
+ .insertFile('META-INF/INDEX.LIST', 'JarIndex-Version: 1.0')
+ .insertFile('META-INF/a.SF', 'Signature File')
+ .insertFile('META-INF/a.DSA', 'DSA Signature Block')
+ .insertFile('META-INF/a.RSA', 'RSA Signature Block')
+ .insertFile('META-INF/a.properties', 'key=value')
+ .publish()
+
+ file('src/main/java/shadow/Passed.java') << '''
+ |package shadow;
+ |public class Passed {}
+ '''.stripMargin()
+
+ buildFile << """
+ |apply plugin: 'java'
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |
+ |repositories { maven { url "${repo.uri}" } }
+ |dependencies { compile 'shadow:a:1.0' }
+ |
+ |shadowJar {
+ | baseName = 'shadow'
+ | classifier = null
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadowJar').build()
+
+ then:
+ contains(output, ['a.properties', 'META-INF/a.properties'])
+
+ and:
+ doesNotContain(output, ['META-INF/INDEX.LIST', 'META-INF/a.SF', 'META-INF/a.DSA', 'META-INF/a.RSA'])
+ }
+
+ def "include runtime configuration by default"() {
+ given:
+ AppendableMavenFileRepository repo = repo()
+
+ repo.module('shadow', 'a', '1.0')
+ .insertFile('a.properties', 'a')
+ .publish()
+
+ repo.module('shadow', 'b', '1.0')
+ .insertFile('b.properties', 'b')
+ .publish()
+
+ buildFile << """
+ |apply plugin: 'java'
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |
+ |repositories { maven { url "${repo.uri}" } }
+ |
+ |dependencies {
+ | runtime 'shadow:a:1.0'
+ | shadow 'shadow:b:1.0'
+ |}
+ |
+ |shadowJar {
+ | baseName = 'shadow'
+ | classifier = null
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadowJar').build()
+
+ then:
+ contains(output, ['a.properties'])
+
+ and:
+ doesNotContain(output, ['b.properties'])
+ }
+
+ def "default copying strategy"() {
+ given:
+ AppendableMavenFileRepository repo = repo()
+
+ repo.module('shadow', 'a', '1.0')
+ .insertFile('META-INF/MANIFEST.MF', 'MANIFEST A')
+ .publish()
+
+ repo.module('shadow', 'b', '1.0')
+ .insertFile('META-INF/MANIFEST.MF', 'MANIFEST B')
+ .publish()
+
+ buildFile << """
+ |apply plugin: 'java'
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |
+ |repositories { maven { url "${repo.uri}" } }
+ |
+ |dependencies {
+ | runtime 'shadow:a:1.0'
+ | runtime 'shadow:b:1.0'
+ |}
+ |
+ |shadowJar {
+ | baseName = 'shadow'
+ | classifier = null
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadowJar').build()
+
+ then:
+ JarFile jar = new JarFile(output)
+ assert jar.entries().collect().size() == 2
+ }
+
+ def "Class-Path in Manifest not added if empty"() {
+ given:
+
+ buildFile << """
+ |apply plugin: 'java'
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |
+ |repositories { maven { url "${repo.uri}" } }
+ |dependencies { compile 'junit:junit:3.8.2' }
+ |
+ |shadowJar {
+ | baseName = 'shadow'
+ | classifier = null
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadowJar').build()
+
+ then:
+ assert output.exists()
+
+ and:
+ JarFile jar = new JarFile(output)
+ Attributes attributes = jar.manifest.getMainAttributes()
+ assert attributes.getValue('Class-Path') == null
+ }
+
+ @Issue('SHADOW-65')
+ def "add shadow configuration to Class-Path in Manifest"() {
+ given:
+
+ buildFile << """
+ |apply plugin: 'java'
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |
+ |repositories { maven { url "${repo.uri}" } }
+ |dependencies { shadow 'junit:junit:3.8.2' }
+ |
+ |jar {
+ | manifest {
+ | attributes 'Class-Path': '/libs/a.jar'
+ | }
+ |}
+ |
+ |shadowJar {
+ | baseName = 'shadow'
+ | classifier = null
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadowJar').build()
+
+ then:
+ assert output.exists()
+
+ and: 'SHADOW-65 - combine w/ existing Class-Path'
+ JarFile jar = new JarFile(output)
+ Attributes attributes = jar.manifest.getMainAttributes()
+ String classpath = attributes.getValue('Class-Path')
+ assert classpath == '/libs/a.jar junit-3.8.2.jar'
+
+ }
+
+ @Issue('SHADOW-92')
+ def "do not include null value in Class-Path when jar file does not contain Class-Path"() {
+ given:
+
+ buildFile << """
+ |apply plugin: 'java'
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |
+ |repositories { maven { url "${repo.uri}" } }
+ |dependencies { shadow 'junit:junit:3.8.2' }
+ |
+ |shadowJar {
+ | baseName = 'shadow'
+ | classifier = null
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadowJar').build()
+
+ then:
+ assert output.exists()
+
+ and:
+ JarFile jar = new JarFile(output)
+ Attributes attributes = jar.manifest.getMainAttributes()
+ String classpath = attributes.getValue('Class-Path')
+ assert classpath == 'junit-3.8.2.jar'
+
+ }
+
+ private String escapedPath(File file) {
+ file.path.replaceAll('\\\\', '\\\\\\\\')
+ }
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/TransformerSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/TransformerSpec.groovy
new file mode 100644
index 0000000..d78e55c
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/TransformerSpec.groovy
@@ -0,0 +1,705 @@
+package com.github.jengelman.gradle.plugins.shadow
+
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
+import com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer
+import com.github.jengelman.gradle.plugins.shadow.transformers.GroovyExtensionModuleTransformer
+import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer
+import com.github.jengelman.gradle.plugins.shadow.transformers.XmlAppendingTransformer
+import com.github.jengelman.gradle.plugins.shadow.util.PluginSpecification
+import spock.lang.Issue
+
+import java.util.jar.JarInputStream
+import java.util.jar.Manifest
+
+class TransformerSpec extends PluginSpecification {
+
+ def 'service resource transformer'() {
+ given:
+ File one = buildJar('one.jar')
+ .insertFile('META-INF/services/org.apache.maven.Shade',
+ 'one # NOTE: No newline terminates this line/file')
+ .insertFile('META-INF/services/com.acme.Foo', 'one')
+ .write()
+
+ File two = buildJar('two.jar')
+ .insertFile('META-INF/services/org.apache.maven.Shade',
+ 'two # NOTE: No newline terminates this line/file')
+ .insertFile('META-INF/services/com.acme.Foo', 'two')
+ .write()
+
+ buildFile << """
+ |task shadow(type: ${ShadowJar.name}) {
+ | destinationDir = new File(buildDir, 'libs')
+ | baseName = 'shadow'
+ | from('${escapedPath(one)}')
+ | from('${escapedPath(two)}')
+ | transform(${ServiceFileTransformer.name}) {
+ | exclude 'META-INF/services/com.acme.*'
+ | }
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadow').build()
+
+ then:
+ assert output.exists()
+
+ and:
+ String text1 = getJarFileContents(output, 'META-INF/services/org.apache.maven.Shade')
+ assert text1.split('(\r\n)|(\r)|(\n)').size() == 2
+ assert text1 == '''|one # NOTE: No newline terminates this line/file
+ |two # NOTE: No newline terminates this line/file'''.stripMargin()
+
+ and:
+ String text2 = getJarFileContents(output, 'META-INF/services/com.acme.Foo')
+ assert text2.split('(\r\n)|(\r)|(\n)').size() == 1
+ assert text2 == 'one'
+ }
+
+ def 'service resource transformer alternate path'() {
+ given:
+ File one = buildJar('one.jar').insertFile('META-INF/foo/org.apache.maven.Shade',
+ 'one # NOTE: No newline terminates this line/file').write()
+
+ File two = buildJar('two.jar').insertFile('META-INF/foo/org.apache.maven.Shade',
+ 'two # NOTE: No newline terminates this line/file').write()
+
+ buildFile << """
+ |task shadow(type: ${ShadowJar.name}) {
+ | destinationDir = new File(buildDir, 'libs')
+ | baseName = 'shadow'
+ | from('${escapedPath(one)}')
+ | from('${escapedPath(two)}')
+ | transform(${ServiceFileTransformer.name}) {
+ | path = 'META-INF/foo'
+ | }
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadow').build()
+
+ then:
+ assert output.exists()
+
+ and:
+ String text = getJarFileContents(output, 'META-INF/foo/org.apache.maven.Shade')
+ assert text.split('(\r\n)|(\r)|(\n)').size() == 2
+ assert text == '''|one # NOTE: No newline terminates this line/file
+ |two # NOTE: No newline terminates this line/file'''.stripMargin()
+ }
+
+ def 'service resource transformer short syntax'() {
+ given:
+ File one = buildJar('one.jar')
+ .insertFile('META-INF/services/org.apache.maven.Shade',
+ 'one # NOTE: No newline terminates this line/file')
+ .insertFile('META-INF/services/com.acme.Foo', 'one')
+ .write()
+
+ File two = buildJar('two.jar')
+ .insertFile('META-INF/services/org.apache.maven.Shade',
+ 'two # NOTE: No newline terminates this line/file')
+ .insertFile('META-INF/services/com.acme.Foo', 'two')
+ .write()
+
+ buildFile << """
+ |task shadow(type: ${ShadowJar.name}) {
+ | destinationDir = new File(buildDir, 'libs')
+ | baseName = 'shadow'
+ | from('${escapedPath(one)}')
+ | from('${escapedPath(two)}')
+ | mergeServiceFiles {
+ | exclude 'META-INF/services/com.acme.*'
+ | }
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadow').build()
+
+ then:
+ assert output.exists()
+
+ and:
+ String text1 = getJarFileContents(output, 'META-INF/services/org.apache.maven.Shade')
+ assert text1.split('(\r\n)|(\r)|(\n)').size() == 2
+ assert text1 == '''|one # NOTE: No newline terminates this line/file
+ |two # NOTE: No newline terminates this line/file'''.stripMargin()
+
+ and:
+ String text2 = getJarFileContents(output, 'META-INF/services/com.acme.Foo')
+ assert text2.split('(\r\n)|(\r)|(\n)').size() == 1
+ assert text2 == 'one'
+ }
+
+ def 'service resource transformer short syntax relocation'() {
+ given:
+ File one = buildJar('one.jar')
+ .insertFile('META-INF/services/java.sql.Driver',
+ '''|oracle.jdbc.OracleDriver
+ |org.apache.hive.jdbc.HiveDriver'''.stripMargin())
+ .insertFile('META-INF/services/org.apache.axis.components.compiler.Compiler',
+ 'org.apache.axis.components.compiler.Javac')
+ .insertFile('META-INF/services/org.apache.commons.logging.LogFactory',
+ 'org.apache.commons.logging.impl.LogFactoryImpl')
+ .write()
+
+ File two = buildJar('two.jar')
+ .insertFile('META-INF/services/java.sql.Driver',
+ '''|org.apache.derby.jdbc.AutoloadedDriver
+ |com.mysql.jdbc.Driver'''.stripMargin())
+ .insertFile('META-INF/services/org.apache.axis.components.compiler.Compiler',
+ 'org.apache.axis.components.compiler.Jikes')
+ .insertFile('META-INF/services/org.apache.commons.logging.LogFactory',
+ 'org.mortbay.log.Factory')
+ .write()
+
+ buildFile << """
+ |task shadow(type: ${ShadowJar.name}) {
+ | destinationDir = new File(buildDir, 'libs')
+ | baseName = 'shadow'
+ | from('${escapedPath(one)}')
+ | from('${escapedPath(two)}')
+ | mergeServiceFiles()
+ | relocate('org.apache', 'myapache') {
+ | exclude 'org.apache.axis.components.compiler.Jikes'
+ | exclude 'org.apache.commons.logging.LogFactory'
+ | }
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadow').build()
+
+ then:
+ assert output.exists()
+
+ and:
+ String text1 = getJarFileContents(output, 'META-INF/services/java.sql.Driver')
+ assert text1.split('(\r\n)|(\r)|(\n)').size() == 4
+ assert text1 == '''|oracle.jdbc.OracleDriver
+ |myapache.hive.jdbc.HiveDriver
+ |myapache.derby.jdbc.AutoloadedDriver
+ |com.mysql.jdbc.Driver'''.stripMargin()
+
+ and:
+ String text2 = getJarFileContents(output, 'META-INF/services/myapache.axis.components.compiler.Compiler')
+ assert text2.split('(\r\n)|(\r)|(\n)').size() == 2
+ assert text2 == '''|myapache.axis.components.compiler.Javac
+ |org.apache.axis.components.compiler.Jikes'''.stripMargin()
+
+ and:
+ String text3 = getJarFileContents(output, 'META-INF/services/org.apache.commons.logging.LogFactory')
+ assert text3.split('(\r\n)|(\r)|(\n)').size() == 2
+ assert text3 == '''|myapache.commons.logging.impl.LogFactoryImpl
+ |org.mortbay.log.Factory'''.stripMargin()
+ }
+
+ def 'service resource transformer short syntax alternate path'() {
+ given:
+ File one = buildJar('one.jar').insertFile('META-INF/foo/org.apache.maven.Shade',
+ 'one # NOTE: No newline terminates this line/file').write()
+
+ File two = buildJar('two.jar').insertFile('META-INF/foo/org.apache.maven.Shade',
+ 'two # NOTE: No newline terminates this line/file').write()
+
+ buildFile << """
+ |task shadow(type: ${ShadowJar.name}) {
+ | destinationDir = new File(buildDir, 'libs')
+ | baseName = 'shadow'
+ | from('${escapedPath(one)}')
+ | from('${escapedPath(two)}')
+ | mergeServiceFiles('META-INF/foo')
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadow').build()
+
+ then:
+ assert output.exists()
+
+ and:
+ String text = getJarFileContents(output, 'META-INF/foo/org.apache.maven.Shade')
+ assert text.split('(\r\n)|(\r)|(\n)').size() == 2
+ assert text == '''|one # NOTE: No newline terminates this line/file
+ |two # NOTE: No newline terminates this line/file'''.stripMargin()
+ }
+
+ @Issue(['SHADOW-70', 'SHADOW-71'])
+ def 'apply transformers to project resources'() {
+ given:
+ File one = buildJar('one.jar').insertFile('META-INF/services/shadow.Shadow',
+ 'one # NOTE: No newline terminates this line/file').write()
+
+ repo.module('shadow', 'two', '1.0').insertFile('META-INF/services/shadow.Shadow',
+ 'two # NOTE: No newline terminates this line/file').publish()
+
+ buildFile << """
+ |apply plugin: 'java'
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |
+ |repositories { maven { url "${repo.uri}" } }
+ |dependencies {
+ | compile 'shadow:two:1.0'
+ | compile files('${escapedPath(one)}')
+ |}
+ |
+ |shadowJar {
+ | baseName = 'shadow'
+ | classifier = null
+ | mergeServiceFiles()
+ |}
+ """.stripMargin()
+
+ file('src/main/resources/META-INF/services/shadow.Shadow') <<
+ 'three # NOTE: No newline terminates this line/file'
+
+ when:
+ runner.withArguments('shadowJar').build()
+
+ then:
+ assert output.exists()
+
+ and:
+ String text = getJarFileContents(output, 'META-INF/services/shadow.Shadow')
+ assert text.split('(\r\n)|(\r)|(\n)').size() == 3
+ assert text == '''|three # NOTE: No newline terminates this line/file
+ |one # NOTE: No newline terminates this line/file
+ |two # NOTE: No newline terminates this line/file'''.stripMargin()
+ }
+
+ def 'appending transformer'() {
+ given:
+ File one = buildJar('one.jar').insertFile('test.properties',
+ 'one # NOTE: No newline terminates this line/file').write()
+
+ File two = buildJar('two.jar').insertFile('test.properties',
+ 'two # NOTE: No newline terminates this line/file').write()
+
+ buildFile << """
+ |task shadow(type: ${ShadowJar.name}) {
+ | destinationDir = new File(buildDir, 'libs')
+ | baseName = 'shadow'
+ | from('${escapedPath(one)}')
+ | from('${escapedPath(two)}')
+ | transform(${AppendingTransformer.name}) {
+ | resource = 'test.properties'
+ | }
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadow').build()
+
+ then:
+ assert output.exists()
+
+ and:
+ String text = getJarFileContents(output, 'test.properties')
+ assert text.split('(\r\n)|(\r)|(\n)').size() == 2
+ assert text == '''|one # NOTE: No newline terminates this line/file
+ |two # NOTE: No newline terminates this line/file
+ |'''.stripMargin()
+ }
+
+ def 'appending transformer short syntax'() {
+ given:
+ File one = buildJar('one.jar').insertFile('test.properties',
+ 'one # NOTE: No newline terminates this line/file').write()
+
+ File two = buildJar('two.jar').insertFile('test.properties',
+ 'two # NOTE: No newline terminates this line/file').write()
+
+ buildFile << """
+ |task shadow(type: ${ShadowJar.name}) {
+ | destinationDir = new File(buildDir, 'libs')
+ | baseName = 'shadow'
+ | from('${escapedPath(one)}')
+ | from('${escapedPath(two)}')
+ | append('test.properties')
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadow').build()
+
+ then:
+ assert output.exists()
+
+ and:
+ String text = getJarFileContents(output, 'test.properties')
+ assert text.split('(\r\n)|(\r)|(\n)').size() == 2
+ assert text == '''|one # NOTE: No newline terminates this line/file
+ |two # NOTE: No newline terminates this line/file
+ |'''.stripMargin()
+ }
+
+ def 'manifest retained'() {
+ given:
+ File main = file('src/main/java/shadow/Main.java')
+ main << '''
+ |package shadow;
+ |
+ |public class Main {
+ |
+ | public static void main(String[] args) { }
+ |}
+ '''.stripMargin()
+
+ buildFile << """
+ |apply plugin: 'java'
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |
+ |jar {
+ | manifest {
+ | attributes 'Main-Class': 'shadow.Main'
+ | attributes 'Test-Entry': 'PASSED'
+ | }
+ |}
+ |
+ |shadowJar {
+ | baseName = 'shadow'
+ | classifier = null
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadowJar').build()
+
+ then:
+ assert output.exists()
+
+ and:
+ JarInputStream jis = new JarInputStream(output.newInputStream())
+ Manifest mf = jis.manifest
+ jis.close()
+
+ assert mf
+ assert mf.mainAttributes.getValue('Test-Entry') == 'PASSED'
+ assert mf.mainAttributes.getValue('Main-Class') == 'shadow.Main'
+ }
+
+ def 'manifest transformed'() {
+ given:
+ File main = file('src/main/java/shadow/Main.java')
+ main << '''
+ |package shadow;
+ |
+ |public class Main {
+ |
+ | public static void main(String[] args) { }
+ |}
+ '''.stripMargin()
+
+ buildFile << """
+ |apply plugin: 'java'
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |
+ |jar {
+ | manifest {
+ | attributes 'Main-Class': 'shadow.Main'
+ | attributes 'Test-Entry': 'FAILED'
+ | }
+ |}
+ |
+ |shadowJar {
+ | baseName = 'shadow'
+ | classifier = null
+ | manifest {
+ | attributes 'Test-Entry': 'PASSED'
+ | attributes 'New-Entry': 'NEW'
+ | }
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadowJar').build()
+
+ then:
+ assert output.exists()
+
+ and:
+ JarInputStream jis = new JarInputStream(output.newInputStream())
+ Manifest mf = jis.manifest
+ jis.close()
+
+ assert mf
+ assert mf.mainAttributes.getValue('Test-Entry') == 'PASSED'
+ assert mf.mainAttributes.getValue('Main-Class') == 'shadow.Main'
+ assert mf.mainAttributes.getValue('New-Entry') == 'NEW'
+ }
+
+ def 'append xml files'() {
+ given:
+ File xml1 = buildJar('xml1.jar').insertFile('properties.xml', '''|<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
+ |
+ |<properties version="1.0">
+ | <entry key="key1">val1</entry>
+ |</properties>
+ |'''.stripMargin()
+ ).write()
+
+ File xml2 = buildJar('xml2.jar').insertFile('properties.xml', '''|<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
+ |
+ |<properties version="1.0">
+ | <entry key="key2">val2</entry>
+ |</properties>
+ |'''.stripMargin()
+ ).write()
+
+ buildFile << """
+ |task shadow(type: ${ShadowJar.name}) {
+ | destinationDir = new File(buildDir, 'libs')
+ | baseName = 'shadow'
+ | from('${escapedPath(xml1)}')
+ | from('${escapedPath(xml2)}')
+ | transform(${XmlAppendingTransformer.name}) {
+ | resource = 'properties.xml'
+ | }
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadow').build()
+
+ then:
+ assert output.exists()
+
+ and:
+ String text = getJarFileContents(output, 'properties.xml')
+ assert text.replaceAll('\r\n', '\n') == '''|<?xml version="1.0" encoding="UTF-8"?>
+ |<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
+ |<properties version="1.0">
+ | <entry key="key1">val1</entry>
+ | <entry key="key2">val2</entry>
+ |</properties>
+ |'''.stripMargin()
+ }
+
+ @Issue('SHADOW-82')
+ def 'shadow.manifest leaks to jar.manifest'() {
+ given:
+ File main = file('src/main/java/shadow/Main.java')
+ main << '''
+ |package shadow;
+ |
+ |public class Main {
+ |
+ | public static void main(String[] args) { }
+ |}
+ '''.stripMargin()
+
+ buildFile << """
+ |apply plugin: 'java'
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |
+ |jar {
+ | baseName = 'jar'
+ | manifest {
+ | attributes 'Main-Class': 'shadow.Main'
+ | attributes 'Test-Entry': 'FAILED'
+ | }
+ |}
+ |
+ |shadowJar {
+ | baseName = 'shadow'
+ | classifier = null
+ | manifest {
+ | attributes 'Test-Entry': 'PASSED'
+ | attributes 'New-Entry': 'NEW'
+ | }
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('jar', 'shadowJar').build()
+
+ then:
+ File jar = file('build/libs/jar.jar')
+ assert jar.exists()
+ assert output.exists()
+
+ then: 'Check contents of Shadow jar manifest'
+ JarInputStream jis = new JarInputStream(output.newInputStream())
+ Manifest mf = jis.manifest
+
+ assert mf
+ assert mf.mainAttributes.getValue('Test-Entry') == 'PASSED'
+ assert mf.mainAttributes.getValue('Main-Class') == 'shadow.Main'
+ assert mf.mainAttributes.getValue('New-Entry') == 'NEW'
+
+ then: 'Check contents of jar manifest'
+ JarInputStream jis2 = new JarInputStream(jar.newInputStream())
+ Manifest mf2 = jis2.manifest
+
+ assert mf2
+ assert mf2.mainAttributes.getValue('Test-Entry') == 'FAILED'
+ assert mf2.mainAttributes.getValue('Main-Class') == 'shadow.Main'
+ assert !mf2.mainAttributes.getValue('New-Entry')
+
+ cleanup:
+ jis?.close()
+ jis2?.close()
+ }
+
+ @Issue('SHADOW-82')
+ def 'shadow manifest leaks to jar manifest'() {
+ given:
+ File main = file('src/main/java/shadow/Main.java')
+ main << '''
+ |package shadow;
+ |
+ |public class Main {
+ |
+ | public static void main(String[] args) { }
+ |}
+ '''.stripMargin()
+
+ buildFile << """
+ |apply plugin: 'java'
+ |apply plugin: 'com.github.johnrengelman.shadow'
+ |
+ |jar {
+ | baseName = 'jar'
+ | manifest {
+ | attributes 'Main-Class': 'shadow.Main'
+ | attributes 'Test-Entry': 'FAILED'
+ | }
+ |}
+ |
+ |shadowJar {
+ | baseName = 'shadow'
+ | classifier = null
+ | manifest {
+ | attributes 'Test-Entry': 'PASSED'
+ | attributes 'New-Entry': 'NEW'
+ | }
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('jar', 'shadowJar').build()
+
+ then:
+ File jar = file('build/libs/jar.jar')
+ assert jar.exists()
+ assert output.exists()
+
+ then: 'Check contents of Shadow jar manifest'
+ JarInputStream jis = new JarInputStream(output.newInputStream())
+ Manifest mf = jis.manifest
+
+ assert mf
+ assert mf.mainAttributes.getValue('Test-Entry') == 'PASSED'
+ assert mf.mainAttributes.getValue('Main-Class') == 'shadow.Main'
+ assert mf.mainAttributes.getValue('New-Entry') == 'NEW'
+
+ then: 'Check contents of jar manifest'
+ JarInputStream jis2 = new JarInputStream(jar.newInputStream())
+ Manifest mf2 = jis2.manifest
+
+ assert mf2
+ assert mf2.mainAttributes.getValue('Test-Entry') == 'FAILED'
+ assert mf2.mainAttributes.getValue('Main-Class') == 'shadow.Main'
+ assert !mf2.mainAttributes.getValue('New-Entry')
+
+ cleanup:
+ jis?.close()
+ jis2?.close()
+ }
+
+ def 'Groovy extension module transformer'() {
+ given:
+ def one = buildJar('one.jar')
+ .insertFile('META-INF/services/org.codehaus.groovy.runtime.ExtensionModule',
+ '''|moduleName=foo
+ |moduleVersion=1.0.5
+ |extensionClasses=com.acme.foo.FooExtension,com.acme.foo.BarExtension
+ |staticExtensionClasses=com.acme.foo.FooStaticExtension'''.stripMargin())
+ .write()
+
+ def two = buildJar('two.jar')
+ .insertFile('META-INF/services/org.codehaus.groovy.runtime.ExtensionModule',
+ '''|moduleName=bar
+ |moduleVersion=2.3.5
+ |extensionClasses=com.acme.bar.SomeExtension,com.acme.bar.AnotherExtension
+ |staticExtensionClasses=com.acme.bar.SomeStaticExtension'''.stripMargin())
+ .write()
+
+ buildFile << """
+ |task shadow(type: ${ShadowJar.name}) {
+ | destinationDir = new File(buildDir, 'libs')
+ | baseName = 'shadow'
+ | from('${escapedPath(one)}')
+ | from('${escapedPath(two)}')
+ | transform(${GroovyExtensionModuleTransformer.name})
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadow').build()
+
+ then:
+ assert output.exists()
+
+ and:
+ def text = getJarFileContents(output, 'META-INF/services/org.codehaus.groovy.runtime.ExtensionModule')
+ def props = new Properties()
+ props.load(new StringReader(text))
+ assert props.getProperty('moduleName') == 'MergedByShadowJar'
+ assert props.getProperty('moduleVersion') == '1.0.0'
+ assert props.getProperty('extensionClasses') == 'com.acme.foo.FooExtension,com.acme.foo.BarExtension,com.acme.bar.SomeExtension,com.acme.bar.AnotherExtension'
+ assert props.getProperty('staticExtensionClasses') == 'com.acme.foo.FooStaticExtension,com.acme.bar.SomeStaticExtension'
+ }
+
+ def 'Groovy extension module transformer short syntax'() {
+ given:
+ def one = buildJar('one.jar')
+ .insertFile('META-INF/services/org.codehaus.groovy.runtime.ExtensionModule',
+ '''|moduleName=foo
+ |moduleVersion=1.0.5
+ |extensionClasses=com.acme.foo.FooExtension,com.acme.foo.BarExtension
+ |staticExtensionClasses=com.acme.foo.FooStaticExtension'''.stripMargin())
+ .write()
+
+ def two = buildJar('two.jar')
+ .insertFile('META-INF/services/org.codehaus.groovy.runtime.ExtensionModule',
+ '''|moduleName=bar
+ |moduleVersion=2.3.5
+ |extensionClasses=com.acme.bar.SomeExtension,com.acme.bar.AnotherExtension
+ |staticExtensionClasses=com.acme.bar.SomeStaticExtension'''.stripMargin())
+ .write()
+
+ buildFile << """
+ |task shadow(type: ${ShadowJar.name}) {
+ | destinationDir = new File(buildDir, 'libs')
+ | baseName = 'shadow'
+ | from('${escapedPath(one)}')
+ | from('${escapedPath(two)}')
+ | mergeGroovyExtensionModules()
+ |}
+ """.stripMargin()
+
+ when:
+ runner.withArguments('shadow').build()
+
+ then:
+ assert output.exists()
+
+ and:
+ def text = getJarFileContents(output, 'META-INF/services/org.codehaus.groovy.runtime.ExtensionModule')
+ def props = new Properties()
+ props.load(new StringReader(text))
+ assert props.getProperty('moduleName') == 'MergedByShadowJar'
+ assert props.getProperty('moduleVersion') == '1.0.0'
+ assert props.getProperty('extensionClasses') == 'com.acme.foo.FooExtension,com.acme.foo.BarExtension,com.acme.bar.SomeExtension,com.acme.bar.AnotherExtension'
+ assert props.getProperty('staticExtensionClasses') == 'com.acme.foo.FooStaticExtension,com.acme.bar.SomeStaticExtension'
+ }
+
+ private String escapedPath(File file) {
+ file.path.replaceAll('\\\\', '\\\\\\\\')
+ }
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorParameterTest.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorParameterTest.groovy
new file mode 100644
index 0000000..7cd0eec
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorParameterTest.groovy
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.relocation
+
+import junit.framework.TestCase
+
+/**
+ * Modified from org.apache.maven.plugins.shade.relocation.SimpleRelocatorParameterTest.java
+ *
+ * Modifications
+ * @author John Engelman
+ */
+class SimpleRelocatorParameterTest extends TestCase {
+
+
+ protected void setUp() {
+ super.setUp()
+ }
+
+ void testThatNullPatternInConstructorShouldNotThrowNullPointerException() {
+ constructThenFailOnNullPointerException(null, "")
+ }
+
+ void testThatNullShadedPatternInConstructorShouldNotThrowNullPointerException() {
+ constructThenFailOnNullPointerException("", null)
+ }
+
+ private void constructThenFailOnNullPointerException(String pattern, String shadedPattern) {
+ try {
+ new SimpleRelocator(pattern, shadedPattern, Collections.<String> emptyList(), Collections.<String> emptyList())
+ }
+ catch (NullPointerException e) {
+ fail("Constructor should not throw null pointer exceptions")
+ }
+ }
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.groovy
new file mode 100644
index 0000000..eea32e6
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.groovy
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.relocation
+
+import junit.framework.TestCase
+
+/**
+ * Test for {@link SimpleRelocator}.
+ *
+ * @author Benjamin Bentmann
+ * @version $Id: SimpleRelocatorTest.java 1342979 2012-05-26 22:05:45Z bimargulies $
+ *
+ * Modified from org.apache.maven.plugins.shade.relocation.SimpleRelocatorTest.java
+ *
+ * Modifications
+ * @author John Engelman
+ */
+class SimpleRelocatorTest extends TestCase {
+
+ void testCanRelocatePath() {
+ SimpleRelocator relocator
+
+ relocator = new SimpleRelocator("org.foo", null, null, null)
+ assertEquals(true, relocator.canRelocatePath("org/foo/Class"))
+ assertEquals(true, relocator.canRelocatePath("org/foo/Class.class"))
+ assertEquals(true, relocator.canRelocatePath("org/foo/bar/Class"))
+ assertEquals(true, relocator.canRelocatePath("org/foo/bar/Class.class"))
+ assertEquals(false, relocator.canRelocatePath("com/foo/bar/Class"))
+ assertEquals(false, relocator.canRelocatePath("com/foo/bar/Class.class"))
+ assertEquals(false, relocator.canRelocatePath("org/Foo/Class"))
+ assertEquals(false, relocator.canRelocatePath("org/Foo/Class.class"))
+
+ relocator = new SimpleRelocator("org.foo", null, null, Arrays.asList(
+ [ "org.foo.Excluded", "org.foo.public.*", "org.foo.Public*Stuff" ] as String[]))
+ assertEquals(true, relocator.canRelocatePath("org/foo/Class"))
+ assertEquals(true, relocator.canRelocatePath("org/foo/Class.class"))
+ assertEquals(true, relocator.canRelocatePath("org/foo/excluded"))
+ assertEquals(false, relocator.canRelocatePath("org/foo/Excluded"))
+ assertEquals(false, relocator.canRelocatePath("org/foo/Excluded.class"))
+ assertEquals(false, relocator.canRelocatePath("org/foo/public"))
+ assertEquals(false, relocator.canRelocatePath("org/foo/public/Class"))
+ assertEquals(false, relocator.canRelocatePath("org/foo/public/Class.class"))
+ assertEquals(true, relocator.canRelocatePath("org/foo/publicRELOC/Class"))
+ assertEquals(true, relocator.canRelocatePath("org/foo/PrivateStuff"))
+ assertEquals(true, relocator.canRelocatePath("org/foo/PrivateStuff.class"))
+ assertEquals(false, relocator.canRelocatePath("org/foo/PublicStuff"))
+ assertEquals(false, relocator.canRelocatePath("org/foo/PublicStuff.class"))
+ assertEquals(false, relocator.canRelocatePath("org/foo/PublicUtilStuff"))
+ assertEquals(false, relocator.canRelocatePath("org/foo/PublicUtilStuff.class"))
+ }
+
+ void testCanRelocateClass() {
+ SimpleRelocator relocator
+
+ relocator = new SimpleRelocator("org.foo", null, null, null)
+ assertEquals(true, relocator.canRelocateClass("org.foo.Class"))
+ assertEquals(true, relocator.canRelocateClass("org.foo.bar.Class"))
+ assertEquals(false, relocator.canRelocateClass("com.foo.bar.Class"))
+ assertEquals(false, relocator.canRelocateClass("org.Foo.Class"))
+
+ relocator = new SimpleRelocator("org.foo", null, null, Arrays.asList(
+ [ "org.foo.Excluded", "org.foo.public.*", "org.foo.Public*Stuff" ] as String[]))
+ assertEquals(true, relocator.canRelocateClass("org.foo.Class"))
+ assertEquals(true, relocator.canRelocateClass("org.foo.excluded"))
+ assertEquals(false, relocator.canRelocateClass("org.foo.Excluded"))
+ assertEquals(false, relocator.canRelocateClass("org.foo.public"))
+ assertEquals(false, relocator.canRelocateClass("org.foo.public.Class"))
+ assertEquals(true, relocator.canRelocateClass("org.foo.publicRELOC.Class"))
+ assertEquals(true, relocator.canRelocateClass("org.foo.PrivateStuff"))
+ assertEquals(false, relocator.canRelocateClass("org.foo.PublicStuff"))
+ assertEquals(false, relocator.canRelocateClass("org.foo.PublicUtilStuff"))
+ }
+
+ void testCanRelocateRawString() {
+ SimpleRelocator relocator
+
+ relocator = new SimpleRelocator("org/foo", null, null, null, true)
+ assertEquals(true, relocator.canRelocatePath("(I)org/foo/bar/Class"))
+
+ relocator = new SimpleRelocator("^META-INF/org.foo.xml\$", null, null, null, true)
+ assertEquals(true, relocator.canRelocatePath("META-INF/org.foo.xml"))
+ }
+
+ //MSHADE-119, make sure that the easy part of this works.
+ void testCanRelocateAbsClassPath() {
+ SimpleRelocator relocator = new SimpleRelocator("org.apache.velocity", "org.apache.momentum", null, null)
+ assertEquals("/org/apache/momentum/mass.properties", relocator.relocatePath("/org/apache/velocity/mass.properties"))
+
+ }
+
+ void testRelocatePath() {
+ SimpleRelocator relocator
+
+ relocator = new SimpleRelocator("org.foo", null, null, null)
+ assertEquals("hidden/org/foo/bar/Class.class", relocator.relocatePath("org/foo/bar/Class.class"))
+
+ relocator = new SimpleRelocator("org.foo", "private.stuff", null, null)
+ assertEquals("private/stuff/bar/Class.class", relocator.relocatePath("org/foo/bar/Class.class"))
+ }
+
+ void testRelocateClass() {
+ SimpleRelocator relocator
+
+ relocator = new SimpleRelocator("org.foo", null, null, null)
+ assertEquals("hidden.org.foo.bar.Class", relocator.relocateClass("org.foo.bar.Class"))
+
+ relocator = new SimpleRelocator("org.foo", "private.stuff", null, null)
+ assertEquals("private.stuff.bar.Class", relocator.relocateClass("org.foo.bar.Class"))
+ }
+
+ void testRelocateRawString() {
+ SimpleRelocator relocator
+
+ relocator = new SimpleRelocator("Lorg/foo", "Lhidden/org/foo", null, null, true)
+ assertEquals("(I)Lhidden/org/foo/bar/Class", relocator.relocatePath("(I)Lorg/foo/bar/Class"))
+
+ relocator = new SimpleRelocator("^META-INF/org.foo.xml\$", "META-INF/hidden.org.foo.xml", null, null, true)
+ assertEquals("META-INF/hidden.org.foo.xml", relocator.relocatePath("META-INF/org.foo.xml"))
+ }
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheLicenseResourceTransformerTest.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheLicenseResourceTransformerTest.groovy
new file mode 100644
index 0000000..1464454
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheLicenseResourceTransformerTest.groovy
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.transformers
+
+import org.junit.Before
+import org.junit.Test
+
+import static org.junit.Assert.*
+
+/**
+ * Test for {@link ApacheLicenseResourceTransformer}.
+ *
+ * @author Benjamin Bentmann
+ * @version $Id: ApacheLicenseResourceTransformerTest.java 673906 2008-07-04 05:03:20Z brett $
+ *
+ * Modified from org.apache.maven.plugins.shade.resources.ApacheLicenseResourceTransformerTest.java
+ */
+class ApacheLicenseResourceTransformerTest extends TransformerTestSupport {
+
+ private ApacheLicenseResourceTransformer transformer
+
+ static {
+ /*
+ * NOTE: The Turkish locale has an usual case transformation for the letters "I" and "i", making it a prime
+ * choice to test for improper case-less string comparisions.
+ */
+ Locale.setDefault(new Locale("tr"))
+ }
+
+ @Before
+ void setUp() {
+ this.transformer = new ApacheLicenseResourceTransformer()
+ }
+
+ @Test
+ void testCanTransformResource() {
+ assertTrue(this.transformer.canTransformResource(getFileElement("META-INF/LICENSE")))
+ assertTrue(this.transformer.canTransformResource(getFileElement("META-INF/LICENSE.TXT")))
+ assertTrue(this.transformer.canTransformResource(getFileElement("META-INF/License.txt")))
+ assertFalse(this.transformer.canTransformResource(getFileElement("META-INF/MANIFEST.MF")))
+ }
+
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformerParameterTests.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformerParameterTests.groovy
new file mode 100644
index 0000000..2826ec3
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformerParameterTests.groovy
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.transformers
+
+import junit.framework.TestCase
+import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator
+
+/**
+ * Tests {@link ApacheLicenseResourceTransformer} parameters.
+ *
+ * Modified from org.apache.maven.plugins.shade.resource.ApacheNoticeResourceTransformerParameterTests.java
+ */
+class ApacheNoticeResourceTransformerParameterTests extends TestCase {
+
+ private static final String NOTICE_RESOURCE = "META-INF/NOTICE"
+ private ApacheNoticeResourceTransformer subject
+
+ protected void setUp() {
+ super.setUp()
+ subject = new ApacheNoticeResourceTransformer()
+ }
+
+ void testNoParametersShouldNotThrowNullPointerWhenNoInput() {
+ processAndFailOnNullPointer("")
+ }
+
+ void testNoParametersShouldNotThrowNullPointerWhenNoLinesOfInput() {
+ processAndFailOnNullPointer("Some notice text")
+ }
+
+ void testNoParametersShouldNotThrowNullPointerWhenOneLineOfInput() {
+ processAndFailOnNullPointer("Some notice text\n")
+ }
+
+ void testNoParametersShouldNotThrowNullPointerWhenTwoLinesOfInput() {
+ processAndFailOnNullPointer("Some notice text\nSome notice text\n")
+ }
+
+ void testNoParametersShouldNotThrowNullPointerWhenLineStartsWithSlashSlash() {
+ processAndFailOnNullPointer("Some notice text\n//Some notice text\n")
+ }
+
+ void testNoParametersShouldNotThrowNullPointerWhenLineIsSlashSlash() {
+ processAndFailOnNullPointer("//\n")
+ }
+
+ void testNoParametersShouldNotThrowNullPointerWhenLineIsEmpty() {
+ processAndFailOnNullPointer("\n")
+ }
+
+ private void processAndFailOnNullPointer(final String noticeText) {
+ try {
+ final ByteArrayInputStream noticeInputStream = new ByteArrayInputStream(noticeText.getBytes())
+ final List<Relocator> emptyList = Collections.emptyList()
+ subject.transform(NOTICE_RESOURCE, noticeInputStream, emptyList)
+ }
+ catch (NullPointerException e) {
+ fail("Null pointer should not be thrown when no parameters are set.")
+ }
+ }
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformerTest.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformerTest.groovy
new file mode 100644
index 0000000..206e719
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformerTest.groovy
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.transformers
+
+import org.junit.Before
+import org.junit.Test
+
+import static org.junit.Assert.*
+
+/**
+ * Test for {@link ApacheNoticeResourceTransformer}.
+ *
+ * @author Benjamin Bentmann
+ * @version $Id: ApacheNoticeResourceTransformerTest.java 673906 2008-07-04 05:03:20Z brett $
+ *
+ * Modified from org.apache.maven.plugins.shade.resource.ApacheNoticeResourceTransformerTest.java
+ */
+class ApacheNoticeResourceTransformerTest extends TransformerTestSupport {
+
+ private ApacheNoticeResourceTransformer transformer
+
+ static {
+ /*
+ * NOTE: The Turkish locale has an usual case transformation for the letters "I" and "i", making it a prime
+ * choice to test for improper case-less string comparisions.
+ */
+ Locale.setDefault(new Locale("tr"))
+ }
+
+ @Before
+ void setUp() {
+ this.transformer = new ApacheNoticeResourceTransformer()
+ }
+
+ @Test
+ void testCanTransformResource() {
+ assertTrue(this.transformer.canTransformResource(getFileElement("META-INF/NOTICE")))
+ assertTrue(this.transformer.canTransformResource(getFileElement("META-INF/NOTICE.TXT")))
+ assertTrue(this.transformer.canTransformResource(getFileElement("META-INF/Notice.txt")))
+ assertFalse(this.transformer.canTransformResource(getFileElement("META-INF/MANIFEST.MF")))
+ }
+
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformerTest.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformerTest.groovy
new file mode 100644
index 0000000..5903ad9
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformerTest.groovy
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.transformers
+
+import org.junit.Before
+import org.junit.Test
+
+import static org.junit.Assert.*
+
+/**
+ * Test for {@link AppendingTransformer}.
+ *
+ * @author Benjamin Bentmann
+ * @version $Id: AppendingTransformerTest.java 673906 2008-07-04 05:03:20Z brett $
+ */
+class AppendingTransformerTest extends TransformerTestSupport {
+
+ private AppendingTransformer transformer
+
+ static
+ {
+ /*
+ * NOTE: The Turkish locale has an usual case transformation for the letters "I" and "i", making it a prime
+ * choice to test for improper case-less string comparisions.
+ */
+ Locale.setDefault(new Locale("tr"))
+ }
+
+ @Before
+ void setUp() {
+ this.transformer = new AppendingTransformer()
+ }
+
+ @Test
+ void testCanTransformResource() {
+ this.transformer.resource = "abcdefghijklmnopqrstuvwxyz"
+
+ assertTrue(this.transformer.canTransformResource(getFileElement("abcdefghijklmnopqrstuvwxyz")))
+ assertTrue(this.transformer.canTransformResource(getFileElement("ABCDEFGHIJKLMNOPQRSTUVWXYZ")))
+ assertFalse(this.transformer.canTransformResource(getFileElement("META-INF/MANIFEST.MF")))
+ }
+
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ComponentsXmlResourceTransformerTest.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ComponentsXmlResourceTransformerTest.groovy
new file mode 100644
index 0000000..7549787
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ComponentsXmlResourceTransformerTest.groovy
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.transformers
+
+import junit.framework.TestCase
+
+import org.custommonkey.xmlunit.Diff
+import org.custommonkey.xmlunit.XMLAssert
+import org.custommonkey.xmlunit.XMLUnit
+import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator
+import org.codehaus.plexus.util.IOUtil
+
+/**
+ * Test for {@link ComponentsXmlResourceTransformer}.
+ *
+ * @author Brett Porter
+ * @version $Id: ComponentsXmlResourceTransformerTest.java 1379994 2012-09-02 15:22:49Z hboutemy $
+ *
+ * Modified from org.apache.maven.plugins.shade.resource.ComponentsXmlResourceTransformerTest.java
+ */
+class ComponentsXmlResourceTransformerTest extends TestCase {
+ private ComponentsXmlResourceTransformer transformer
+
+ void setUp() {
+ this.transformer = new ComponentsXmlResourceTransformer()
+ }
+
+ void testConfigurationMerging() {
+
+ XMLUnit.setNormalizeWhitespace(true)
+
+ transformer.transform("components-1.xml", getClass().getResourceAsStream("/components-1.xml"),
+ Collections.<Relocator> emptyList())
+ transformer.transform("components-1.xml", getClass().getResourceAsStream("/components-2.xml"),
+ Collections.<Relocator> emptyList())
+ Diff diff = XMLUnit.compareXML(
+ IOUtil.toString(getClass().getResourceAsStream("/components-expected.xml"), "UTF-8"),
+ IOUtil.toString(transformer.getTransformedResource(), "UTF-8"))
+ //assertEquals( IOUtil.toString( getClass().getResourceAsStream( "/components-expected.xml" ), "UTF-8" ),
+ // IOUtil.toString( transformer.getTransformedResource(), "UTF-8" ).replaceAll("\r\n", "\n") )
+ XMLAssert.assertXMLIdentical(diff, true)
+ }
+}
\ No newline at end of file
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformerSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformerSpec.groovy
new file mode 100644
index 0000000..d0dd61b
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformerSpec.groovy
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.transformers
+
+import spock.lang.Unroll
+
+ at Unroll
+class PropertiesFileTransformerSpec extends TransformerSpecSupport {
+
+ void "Path #path #transform transformed"() {
+ given:
+ Transformer transformer = new PropertiesFileTransformer()
+
+ when:
+ boolean actual = transformer.canTransformResource(getFileElement(path))
+
+ then:
+ actual == expected
+
+ where:
+ path || expected
+ 'foo.properties' || true
+ 'foo/bar.properties' || true
+ 'foo.props' || false
+
+ transform = expected ? 'can be' : 'can not be'
+ }
+
+ void exerciseAllTransformConfigurations() {
+ given:
+ def element = getFileElement(path)
+ Transformer transformer = new PropertiesFileTransformer()
+ transformer.mergeStrategy = mergeStrategy
+ transformer.mergeSeparator = mergeSeparator
+
+ when:
+ if (transformer.canTransformResource(element)) {
+ transformer.transform(path, toInputStream(toProperties(input1)), [])
+ transformer.transform(path, toInputStream(toProperties(input2)), [])
+ }
+
+ then:
+ output == toMap(transformer.propertiesEntries[path])
+
+ where:
+ path | mergeStrategy | mergeSeparator | input1 | input2 || output
+ 'f.properties' | 'first' | '' | ['foo': 'foo'] | ['foo': 'bar'] || ['foo': 'foo']
+ 'f.properties' | 'latest' | '' | ['foo': 'foo'] | ['foo': 'bar'] || ['foo': 'bar']
+ 'f.properties' | 'append' | ',' | ['foo': 'foo'] | ['foo': 'bar'] || ['foo': 'foo,bar']
+ 'f.properties' | 'append' | ';' | ['foo': 'foo'] | ['foo': 'bar'] || ['foo': 'foo;bar']
+ }
+
+ void exerciseAllTransformConfigurationsWithPaths() {
+ given:
+ def element = getFileElement(path)
+ Transformer transformer = new PropertiesFileTransformer()
+ transformer.paths = paths
+ transformer.mergeStrategy = 'first'
+
+ when:
+ if (transformer.canTransformResource(element)) {
+ transformer.transform(path, toInputStream(toProperties(input1)), [])
+ transformer.transform(path, toInputStream(toProperties(input2)), [])
+ }
+
+ then:
+ output == toMap(transformer.propertiesEntries[path])
+
+ where:
+ path | paths | input1 | input2 || output
+ 'f.properties' | ['f.properties'] | ['foo': 'foo'] | ['foo': 'bar'] || ['foo': 'foo']
+ 'foo.properties' | ['.*.properties'] | ['foo': 'foo'] | ['foo': 'bar'] || ['foo': 'foo']
+ 'foo.properties' | ['.*bar'] | ['foo': 'foo'] | ['foo': 'bar'] || [:]
+ 'foo.properties' | [] | ['foo': 'foo'] | ['foo': 'bar'] || ['foo': 'foo']
+ }
+
+ void exerciseAllTransformConfigurationsWithMappings() {
+ given:
+ def element = getFileElement(path)
+ Transformer transformer = new PropertiesFileTransformer()
+ transformer.mappings = mappings
+ transformer.mergeStrategy = 'latest'
+
+ when:
+ if (transformer.canTransformResource(element)) {
+ transformer.transform(path, toInputStream(toProperties(input1)), [])
+ transformer.transform(path, toInputStream(toProperties(input2)), [])
+ }
+
+ then:
+ output == toMap(transformer.propertiesEntries[path])
+
+ where:
+ path | mappings | input1 | input2 || output
+ 'f.properties' | ['f.properties': [mergeStrategy: 'first']] | ['foo': 'foo'] | ['foo': 'bar'] || ['foo': 'foo']
+ 'f.properties' | ['f.properties': [mergeStrategy: 'latest']] | ['foo': 'foo'] | ['foo': 'bar'] || ['foo': 'bar']
+ 'f.properties' | ['f.properties': [mergeStrategy: 'append']] | ['foo': 'foo'] | ['foo': 'bar'] || ['foo': 'foo,bar']
+ 'f.properties' | ['f.properties': [mergeStrategy: 'append', mergeSeparator: ';']] | ['foo': 'foo'] | ['foo': 'bar'] || ['foo': 'foo;bar']
+ 'foo.properties' | ['.*.properties': [mergeStrategy: 'first']] | ['foo': 'foo'] | ['foo': 'bar'] || ['foo': 'foo']
+ 'foo.properties' | ['.*bar': [mergeStrategy: 'first']] | ['foo': 'foo'] | ['foo': 'bar'] || [:]
+ }
+
+ private static InputStream toInputStream(Properties props) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream()
+ props.store(baos, '')
+ new ByteArrayInputStream(baos.toByteArray())
+ }
+
+ private static Properties toProperties(Map map) {
+ map.inject(new Properties()) { Properties props, entry ->
+ props.put(entry.key, entry.value)
+ props
+ }
+ }
+
+ private static Map toMap(Properties props) {
+ props.inject([:]) { Map map, entry ->
+ map.put(entry.key, entry.value)
+ map
+ }
+ }
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformerSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformerSpec.groovy
new file mode 100644
index 0000000..d91670c
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformerSpec.groovy
@@ -0,0 +1,68 @@
+package com.github.jengelman.gradle.plugins.shadow.transformers
+
+import spock.lang.Unroll
+
+ at Unroll
+class ServiceFileTransformerSpec extends TransformerSpecSupport {
+
+ def "#status path #path #transform transformed"() {
+ given:
+ def transformer = new ServiceFileTransformer()
+ if (exclude) {
+ transformer.exclude(path)
+ }
+
+ when:
+ def actual = transformer.canTransformResource(getFileElement(path))
+
+ then:
+ actual == expected
+
+ where:
+ path | exclude | expected
+ 'META-INF/services/java.sql.Driver' | false | true
+ 'META-INF/services/io.dropwizard.logging.AppenderFactory' | false | true
+ 'META-INF/services/org.apache.maven.Shade' | true | false
+ 'META-INF/services/foo/bar/moo.goo.Zoo' | false | true
+ 'foo/bar.properties' | false | false
+ 'foo.props' | false | false
+
+ transform = expected ? 'can be' : 'can not be'
+ status = exclude ? 'excluded' : 'non-excluded'
+ }
+
+ def "transforms service file"() {
+ given:
+ def element = getFileElement(path)
+ def transformer = new ServiceFileTransformer()
+
+ when:
+ if (transformer.canTransformResource(element)) {
+ transformer.transform(path, toInputStream(input1), [])
+ transformer.transform(path, toInputStream(input2), [])
+ }
+
+ then:
+ transformer.hasTransformedResource()
+ output == transformer.serviceEntries[path].toInputStream().text
+
+ where:
+ path | input1 | input2 || output
+ 'META-INF/services/com.acme.Foo' | 'foo' | 'bar' || 'foo\nbar'
+ 'META-INF/services/com.acme.Bar' | 'foo\nbar' | 'zoo' || 'foo\nbar\nzoo'
+ }
+
+ def "excludes Groovy extension module descriptor files by default"() {
+ given:
+ def transformer = new ServiceFileTransformer()
+ def element = getFileElement('META-INF/services/org.codehaus.groovy.runtime.ExtensionModule')
+
+ expect:
+ !transformer.canTransformResource(element)
+ }
+
+ private static InputStream toInputStream(String str) {
+ return new ByteArrayInputStream(str.bytes)
+ }
+
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/TransformerSpecSupport.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/TransformerSpecSupport.groovy
new file mode 100644
index 0000000..a816111
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/TransformerSpecSupport.groovy
@@ -0,0 +1,14 @@
+package com.github.jengelman.gradle.plugins.shadow.transformers
+
+import org.gradle.api.file.FileTreeElement
+import org.gradle.api.file.RelativePath
+import org.gradle.api.internal.file.DefaultFileTreeElement
+import spock.lang.Specification
+
+class TransformerSpecSupport extends Specification {
+
+ protected static FileTreeElement getFileElement(String path) {
+ return new DefaultFileTreeElement(null, RelativePath.parse(true, path), null, null)
+ }
+
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/TransformerTestSupport.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/TransformerTestSupport.groovy
new file mode 100644
index 0000000..c364fa1
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/TransformerTestSupport.groovy
@@ -0,0 +1,13 @@
+package com.github.jengelman.gradle.plugins.shadow.transformers
+
+import org.gradle.api.file.FileTreeElement
+import org.gradle.api.file.RelativePath
+import org.gradle.api.internal.file.DefaultFileTreeElement
+
+class TransformerTestSupport {
+
+ protected static FileTreeElement getFileElement(String path) {
+ return new DefaultFileTreeElement(null, RelativePath.parse(true, path), null, null)
+ }
+
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/XmlAppendingTransformerTest.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/XmlAppendingTransformerTest.groovy
new file mode 100644
index 0000000..c94780f
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/XmlAppendingTransformerTest.groovy
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License") you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.transformers
+
+import org.junit.Before
+import org.junit.Test
+
+import static org.junit.Assert.*
+
+/**
+ * Test for {@link XmlAppendingTransformer}.
+ *
+ * @author Benjamin Bentmann
+ * @version $Id: XmlAppendingTransformerTest.java 673906 2008-07-04 05:03:20Z brett $
+ *
+ * Modified from org.apache.maven.plugins.shade.resource.XmlAppendingTransformerTest.java
+ */
+class XmlAppendingTransformerTest extends TransformerTestSupport {
+
+ XmlAppendingTransformer transformer
+
+ static {
+ /*
+ * NOTE: The Turkish locale has an usual case transformation for the letters "I" and "i", making it a prime
+ * choice to test for improper case-less string comparisons.
+ */
+ Locale.setDefault(new Locale("tr"))
+ }
+
+ @Before
+ void setUp() {
+ transformer = new XmlAppendingTransformer()
+ }
+
+ @Test
+ void testCanTransformResource() {
+ transformer.resource = "abcdefghijklmnopqrstuvwxyz"
+
+ assertTrue(this.transformer.canTransformResource(getFileElement("abcdefghijklmnopqrstuvwxyz")))
+ assertTrue(this.transformer.canTransformResource(getFileElement("ABCDEFGHIJKLMNOPQRSTUVWXYZ")))
+ assertFalse(this.transformer.canTransformResource(getFileElement("META-INF/MANIFEST.MF")))
+ }
+
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/AppendableJar.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/AppendableJar.groovy
new file mode 100644
index 0000000..b6fad70
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/AppendableJar.groovy
@@ -0,0 +1,25 @@
+package com.github.jengelman.gradle.plugins.shadow.util
+
+class AppendableJar {
+
+ Map<String, String> contents = [:]
+ File file
+
+ AppendableJar(File file) {
+ this.file = file
+ }
+
+ AppendableJar insertFile(String path, String content) {
+ contents[path] = content
+ return this
+ }
+
+ File write() {
+ JarBuilder builder = new JarBuilder(file.newOutputStream())
+ contents.each { path, contents ->
+ builder.withFile(path, contents)
+ }
+ builder.build()
+ return file
+ }
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/AppendableMavenFileModule.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/AppendableMavenFileModule.groovy
new file mode 100644
index 0000000..7d60400
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/AppendableMavenFileModule.groovy
@@ -0,0 +1,70 @@
+package com.github.jengelman.gradle.plugins.shadow.util
+
+import com.github.jengelman.gradle.plugins.shadow.util.repo.maven.MavenFileModule
+import groovy.transform.InheritConstructors
+import org.apache.commons.io.IOUtils
+
+ at InheritConstructors
+class AppendableMavenFileModule extends MavenFileModule {
+
+ Map<String, Map<String, String>> contents = [:].withDefault { [:] }
+ Map<String, File> files = [:]
+
+ AppendableMavenFileModule use(File file) {
+ return use('', file)
+ }
+
+ AppendableMavenFileModule use(String classifier, File file) {
+ files[classifier] = file
+ return this
+ }
+
+ AppendableMavenFileModule insertFile(String path, String content) {
+ insertFile('', path, content)
+ return this
+ }
+
+ AppendableMavenFileModule insertFile(String classifier, String path, String content) {
+ contents[classifier][path] = content
+ return this
+ }
+
+ @Override
+ File publishArtifact(Map<String, ?> artifact) {
+ def artifactFile = artifactFile(artifact)
+ if (type == 'pom') {
+ return artifactFile
+ }
+ String classifier = (String) artifact['classifier'] ?: ''
+ if (files.containsKey(classifier)) {
+ publishWithStream(artifactFile) { OutputStream os ->
+ IOUtils.copy(files[classifier].newInputStream(), os)
+ }
+ } else {
+ publishWithStream(artifactFile) { OutputStream os ->
+ writeJar(os, contents[classifier])
+ }
+ }
+ return artifactFile
+ }
+
+ void writeJar(OutputStream os, Map<String, String> contents) {
+ if (contents) {
+ JarBuilder builder = new JarBuilder(os)
+ contents.each { path, content ->
+ builder.withFile(path, content)
+ }
+ builder.build()
+ }
+ }
+
+ /**
+ * Adds an additional artifact to this module.
+ * @param options Can specify any of: type or classifier
+ */
+ AppendableMavenFileModule artifact(Map<String, ?> options) {
+ artifacts << options
+ return this
+ }
+
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/AppendableMavenFileRepository.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/AppendableMavenFileRepository.groovy
new file mode 100644
index 0000000..c923147
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/AppendableMavenFileRepository.groovy
@@ -0,0 +1,15 @@
+package com.github.jengelman.gradle.plugins.shadow.util
+
+import com.github.jengelman.gradle.plugins.shadow.util.repo.maven.MavenFileRepository
+import groovy.transform.InheritConstructors
+
+ at InheritConstructors
+class AppendableMavenFileRepository extends MavenFileRepository {
+
+ @Override
+ AppendableMavenFileModule module(String groupId, String artifactId, Object version = '1.0') {
+ def artifactDir = rootDir.file("${groupId.replace('.', '/')}/$artifactId/$version")
+ return new AppendableMavenFileModule(artifactDir, groupId, artifactId, version as String)
+ }
+
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/JarBuilder.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/JarBuilder.groovy
new file mode 100644
index 0000000..b24eb99
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/JarBuilder.groovy
@@ -0,0 +1,51 @@
+package com.github.jengelman.gradle.plugins.shadow.util
+
+import org.codehaus.plexus.util.IOUtil
+
+import java.util.jar.JarEntry
+import java.util.jar.JarOutputStream
+
+class JarBuilder {
+
+ List<String> entries = []
+ JarOutputStream jos
+
+ JarBuilder(OutputStream os) {
+ jos = new JarOutputStream(os)
+ }
+
+ private void addDirectory(String name) {
+ if (!entries.contains(name)) {
+ if (name.lastIndexOf('/') > 0) {
+ String parent = name.substring(0, name.lastIndexOf('/'))
+ if (!entries.contains(parent)) {
+ addDirectory(parent)
+ }
+ }
+
+ // directory entries must end in "/"
+ JarEntry entry = new JarEntry(name + "/")
+ jos.putNextEntry(entry)
+
+ entries.add(name)
+ }
+ }
+
+ JarBuilder withFile(String path, String data) {
+ def idx = path.lastIndexOf('/')
+ if (idx != -1) {
+ addDirectory(path.substring(0, idx))
+ }
+ if (!entries.contains(path)) {
+ JarEntry entry = new JarEntry(path)
+ jos.putNextEntry(entry)
+ entries << path
+ IOUtil.copy(data.bytes, jos)
+ }
+ return this
+ }
+
+ void build() {
+ jos.close()
+ }
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/PluginSpecification.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/PluginSpecification.groovy
new file mode 100644
index 0000000..50502e7
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/PluginSpecification.groovy
@@ -0,0 +1,127 @@
+package com.github.jengelman.gradle.plugins.shadow.util
+
+import com.github.jengelman.gradle.plugins.shadow.util.file.TestFile
+import com.google.common.base.StandardSystemProperty
+import org.codehaus.plexus.util.IOUtil
+import org.gradle.testkit.runner.GradleRunner
+import org.junit.Rule
+import org.junit.rules.TemporaryFolder
+import spock.lang.Specification
+
+import java.util.jar.JarEntry
+import java.util.jar.JarFile
+
+class PluginSpecification extends Specification {
+
+ @Rule TemporaryFolder dir
+
+ private static final String SHADOW_VERSION = PluginSpecification.classLoader.getResource("shadow-version.txt").text.trim()
+
+ AppendableMavenFileRepository repo
+
+ def setup() {
+ repo = repo()
+ repo.module('junit', 'junit', '3.8.2').use(testJar).publish()
+
+ buildFile << defaultBuildScript
+ }
+
+ String getDefaultBuildScript() {
+ return """
+ |buildscript {
+ | repositories {
+ | //maven { url "${localRepo.toURI()}" }
+ | mavenLocal()
+ | jcenter()
+ | }
+ | dependencies {
+ | classpath 'com.github.jengelman.gradle.plugins:shadow:${SHADOW_VERSION}'
+ | }
+ |}
+ """.stripMargin()
+ }
+
+ GradleRunner getRunner() {
+ GradleRunner.create()
+ .withProjectDir(dir.root)
+ .forwardOutput()
+ .withDebug(true)
+ .withTestKitDir(getTestKitDir())
+ }
+
+ File getLocalRepo() {
+ def rootRelative = new File("build/localrepo")
+ rootRelative.directory ? rootRelative : new File(new File(StandardSystemProperty.USER_DIR.value()).parentFile, "build/localrepo")
+ }
+
+ File getBuildFile() {
+ file('build.gradle')
+ }
+
+ File getSettingsFile() {
+ file('settings.gradle')
+ }
+
+ File file(String path) {
+ File f = new File(dir.root, path)
+ if (!f.exists()) {
+ f.parentFile.mkdirs()
+ return dir.newFile(path)
+ }
+ return f
+ }
+
+ AppendableMavenFileRepository repo(String path = 'maven-repo') {
+ new AppendableMavenFileRepository(new TestFile(dir.root, path))
+ }
+
+ void assertJarFileContentsEqual(File f, String path, String contents) {
+ assert getJarFileContents(f, path) == contents
+ }
+
+ String getJarFileContents(File f, String path) {
+ JarFile jf = new JarFile(f)
+ def is = jf.getInputStream(new JarEntry(path))
+ StringWriter sw = new StringWriter()
+ IOUtil.copy(is, sw)
+ is.close()
+ jf.close()
+ return sw.toString()
+ }
+
+ void contains(File f, List<String> paths) {
+ JarFile jar = new JarFile(f)
+ paths.each { path ->
+ assert jar.getJarEntry(path), "${f.path} does not contain [$path]"
+ }
+ jar.close()
+ }
+
+ void doesNotContain(File f, List<String> paths) {
+ JarFile jar = new JarFile(f)
+ paths.each { path ->
+ assert !jar.getJarEntry(path), "${f.path} contains [$path]"
+ }
+ jar.close()
+ }
+
+ AppendableJar buildJar(String path) {
+ return new AppendableJar(file(path))
+ }
+
+ protected getOutput() {
+ file('build/libs/shadow.jar')
+ }
+
+ protected File getTestJar(String name = 'junit-3.8.2.jar') {
+ return new File(this.class.classLoader.getResource(name).toURI())
+ }
+
+ public static File getTestKitDir() {
+ def gradleUserHome = System.getenv("GRADLE_USER_HOME")
+ if (!gradleUserHome) {
+ gradleUserHome = new File(System.getProperty("user.home"), ".gradle").absolutePath
+ }
+ return new File(gradleUserHome, "testkit")
+ }
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/file/ExecOutput.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/file/ExecOutput.groovy
new file mode 100644
index 0000000..3bd0f93
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/file/ExecOutput.groovy
@@ -0,0 +1,13 @@
+package com.github.jengelman.gradle.plugins.shadow.util.file
+
+class ExecOutput {
+ ExecOutput(String rawOutput, String error) {
+ this.rawOutput = rawOutput
+ this.out = rawOutput.replaceAll("\r\n|\r", "\n")
+ this.error = error
+ }
+
+ String rawOutput
+ String out
+ String error
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/file/Results.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/file/Results.groovy
new file mode 100644
index 0000000..19cfb8f
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/file/Results.groovy
@@ -0,0 +1,58 @@
+package com.github.jengelman.gradle.plugins.shadow.util.file
+
+import org.gradle.tooling.GradleConnectionException
+import org.gradle.tooling.ResultHandler
+
+class Results implements ResultHandler<Void> {
+
+ private final Object lock = new Object()
+
+ private boolean success = false
+ private GradleConnectionException exception
+
+ void waitForCompletion() {
+ synchronized(lock) {
+ while(!successful && !failed) {
+ lock.wait()
+ }
+ }
+ }
+
+ boolean getSuccessful() {
+ return success && !exception
+ }
+
+ boolean getFailed() {
+ return exception as boolean
+ }
+
+ GradleConnectionException getException() {
+ return exception
+ }
+
+ void markComplete() {
+ synchronized(lock) {
+ success = true
+ exception = null
+ lock.notifyAll()
+ }
+ }
+
+ void markFailed(GradleConnectionException e) {
+ synchronized(lock) {
+ success = false
+ exception = e
+ lock.notifyAll()
+ }
+ }
+
+ @Override
+ void onComplete(Void aVoid) {
+ markComplete()
+ }
+
+ @Override
+ void onFailure(GradleConnectionException e) {
+ markFailed(e)
+ }
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/file/TestDirectoryProvider.java b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/file/TestDirectoryProvider.java
new file mode 100644
index 0000000..1797816
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/file/TestDirectoryProvider.java
@@ -0,0 +1,17 @@
+package com.github.jengelman.gradle.plugins.shadow.util.file;
+
+/**
+ * Implementations provide a working space to be used in tests.
+ *
+ * The client is not responsible for removing any files.
+ */
+public interface TestDirectoryProvider {
+
+ /**
+ * The directory to use, guaranteed to exist.
+ *
+ * @return The directory to use, guaranteed to exist.
+ */
+ TestFile getTestDirectory();
+
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/file/TestFile.java b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/file/TestFile.java
new file mode 100644
index 0000000..b109bd3
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/file/TestFile.java
@@ -0,0 +1,573 @@
+/*
+ * Copyright 2010 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.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.
+ */
+
+package com.github.jengelman.gradle.plugins.shadow.util.file;
+
+import groovy.lang.Closure;
+import org.apache.commons.io.FileUtils;
+import org.apache.tools.ant.Project;
+import org.apache.tools.ant.Task;
+import org.apache.tools.ant.taskdefs.Tar;
+import org.apache.tools.ant.taskdefs.Zip;
+import org.apache.tools.ant.types.EnumeratedAttribute;
+import org.codehaus.groovy.runtime.ResourceGroovyMethods;
+import org.hamcrest.Matcher;
+
+import java.io.*;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.security.MessageDigest;
+import java.util.*;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+
+import static org.junit.Assert.*;
+
+public class TestFile extends File {
+ private boolean useNativeTools;
+
+ public TestFile(File file, Object... path) {
+ super(join(file, path).getAbsolutePath());
+ }
+
+ public TestFile(URI uri) {
+ this(new File(uri));
+ }
+
+ public TestFile(String path) {
+ this(new File(path));
+ }
+
+ public TestFile(URL url) {
+ this(toUri(url));
+ }
+
+ public TestFile usingNativeTools() {
+ useNativeTools = true;
+ return this;
+ }
+
+ Object writeReplace() throws ObjectStreamException {
+ return new File(getAbsolutePath());
+ }
+
+ @Override
+ public File getCanonicalFile() throws IOException {
+ return new File(getAbsolutePath()).getCanonicalFile();
+ }
+
+ @Override
+ public String getCanonicalPath() throws IOException {
+ return new File(getAbsolutePath()).getCanonicalPath();
+ }
+
+ private static URI toUri(URL url) {
+ try {
+ return url.toURI();
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static File join(File file, Object[] path) {
+ File current = file.getAbsoluteFile();
+ for (Object p : path) {
+ current = new File(current, p.toString());
+ }
+ try {
+ return current.getCanonicalFile();
+ } catch (IOException e) {
+ throw new RuntimeException(String.format("Could not canonicalise '%s'.", current), e);
+ }
+ }
+
+ public TestFile file(Object... path) {
+ try {
+ return new TestFile(this, path);
+ } catch (RuntimeException e) {
+ throw new RuntimeException(String.format("Could not locate file '%s' relative to '%s'.", Arrays.toString(path), this), e);
+ }
+ }
+
+ public List<TestFile> files(Object... paths) {
+ List<TestFile> files = new ArrayList<TestFile>();
+ for (Object path : paths) {
+ files.add(file(path));
+ }
+ return files;
+ }
+
+ public TestFile withExtension(String extension) {
+ return getParentFile().file(getName().replaceAll("\\..*$", "." + extension));
+ }
+
+ public TestFile writelns(String... lines) {
+ return writelns(Arrays.asList(lines));
+ }
+
+ public TestFile write(Object content) {
+ try {
+ FileUtils.writeStringToFile(this, content.toString());
+ } catch (IOException e) {
+ throw new RuntimeException(String.format("Could not write to test file '%s'", this), e);
+ }
+ return this;
+ }
+
+ public TestFile leftShift(Object content) {
+ getParentFile().mkdirs();
+ try {
+ ResourceGroovyMethods.leftShift(this, content);
+ return this;
+ } catch (IOException e) {
+ throw new RuntimeException(String.format("Could not append to test file '%s'", this), e);
+ }
+ }
+
+ public TestFile[] listFiles() {
+ File[] children = super.listFiles();
+ TestFile[] files = new TestFile[children.length];
+ for (int i = 0; i < children.length; i++) {
+ File child = children[i];
+ files[i] = new TestFile(child);
+ }
+ return files;
+ }
+
+ public String getText() {
+ assertIsFile();
+ try {
+ return FileUtils.readFileToString(this);
+ } catch (IOException e) {
+ throw new RuntimeException(String.format("Could not read from test file '%s'", this), e);
+ }
+ }
+
+ public Map<String, String> getProperties() {
+ assertIsFile();
+ Properties properties = new Properties();
+ try {
+ FileInputStream inStream = new FileInputStream(this);
+ try {
+ properties.load(inStream);
+ } finally {
+ inStream.close();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ Map<String, String> map = new HashMap<String, String>();
+ for (Object key : properties.keySet()) {
+ map.put(key.toString(), properties.getProperty(key.toString()));
+ }
+ return map;
+ }
+
+ public Manifest getManifest() {
+ assertIsFile();
+ try {
+ JarFile jarFile = new JarFile(this);
+ try {
+ return jarFile.getManifest();
+ } finally {
+ jarFile.close();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public List<String> linesThat(Matcher<? super String> matcher) {
+ try {
+ BufferedReader reader = new BufferedReader(new FileReader(this));
+ try {
+ List<String> lines = new ArrayList<String>();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ if (matcher.matches(line)) {
+ lines.add(line);
+ }
+ }
+ return lines;
+ } finally {
+ reader.close();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void unzipTo(File target) {
+ assertIsFile();
+ new TestFileHelper(this).unzipTo(target, useNativeTools);
+ }
+
+ public void untarTo(File target) {
+ assertIsFile();
+
+ new TestFileHelper(this).untarTo(target, useNativeTools);
+ }
+
+ public void copyTo(File target) {
+ if (isDirectory()) {
+ try {
+ FileUtils.copyDirectory(this, target);
+ } catch (IOException e) {
+ throw new RuntimeException(String.format("Could not copy test directory '%s' to '%s'", this,
+ target), e);
+ }
+ } else {
+ try {
+ FileUtils.copyFile(this, target);
+ } catch (IOException e) {
+ throw new RuntimeException(String.format("Could not copy test file '%s' to '%s'", this, target), e);
+ }
+ }
+ }
+
+ public void copyFrom(File target) {
+ new TestFile(target).copyTo(this);
+ }
+
+ public void copyFrom(URL resource) {
+ try {
+ FileUtils.copyURLToFile(resource, this);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void moveToDirectory(File target) {
+ if (target.exists() && !target.isDirectory()) {
+ throw new RuntimeException(String.format("Target '%s' is not a directory", target));
+ }
+ try {
+ FileUtils.moveFileToDirectory(this, target, true);
+ } catch (IOException e) {
+ throw new RuntimeException(String.format("Could not move test file '%s' to directory '%s'", this, target), e);
+ }
+ }
+
+ public TestFile touch() {
+ try {
+ FileUtils.touch(this);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ assertIsFile();
+ return this;
+ }
+
+ /**
+ * Creates a directory structure specified by the given closure.
+ * <pre>
+ * dir.create {
+ * subdir1 {
+ * file 'somefile.txt'
+ * }
+ * subdir2 { nested { file 'someFile' } }
+ * }
+ * </pre>
+ */
+ public TestFile create(Closure structure) {
+ assertTrue(isDirectory() || mkdirs());
+ new TestWorkspaceBuilder(this).apply(structure);
+ return this;
+ }
+
+ @Override
+ public TestFile getParentFile() {
+ return super.getParentFile() == null ? null : new TestFile(super.getParentFile());
+ }
+
+ @Override
+ public String toString() {
+ return getPath();
+ }
+
+ public TestFile writelns(Iterable<String> lines) {
+ Formatter formatter = new Formatter();
+ for (String line : lines) {
+ formatter.format("%s%n", line);
+ }
+ return write(formatter);
+ }
+
+ public TestFile assertExists() {
+ assertTrue(String.format("%s does not exist", this), exists());
+ return this;
+ }
+
+ public TestFile assertIsFile() {
+ assertTrue(String.format("%s is not a file", this), isFile());
+ return this;
+ }
+
+ public TestFile assertIsDir() {
+ assertTrue(String.format("%s is not a directory", this), isDirectory());
+ return this;
+ }
+
+ public TestFile assertDoesNotExist() {
+ assertFalse(String.format("%s should not exist", this), exists());
+ return this;
+ }
+
+ public TestFile assertContents(Matcher<String> matcher) {
+ assertThat(getText(), matcher);
+ return this;
+ }
+
+ public TestFile assertIsCopyOf(TestFile other) {
+ assertIsFile();
+ other.assertIsFile();
+ assertEquals(String.format("%s is not the same length as %s", this, other), other.length(), this.length());
+ assertTrue(String.format("%s does not have the same content as %s", this, other), Arrays.equals(getHash("MD5"), other.getHash("MD5")));
+ return this;
+ }
+
+ private byte[] getHash(String algorithm) {
+ try {
+ MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
+ messageDigest.update(FileUtils.readFileToByteArray(this));
+ return messageDigest.digest();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public String readLink() {
+ assertExists();
+ return new TestFileHelper(this).readLink();
+ }
+
+ public String getPermissions() {
+ assertExists();
+ return new TestFileHelper(this).getPermissions();
+ }
+
+ public TestFile setPermissions(String permissions) {
+ assertExists();
+ new TestFileHelper(this).setPermissions(permissions);
+ return this;
+ }
+
+ public TestFile setMode(int mode) {
+ assertExists();
+ new TestFileHelper(this).setMode(mode);
+ return this;
+ }
+
+ public int getMode() {
+ assertExists();
+ return new TestFileHelper(this).getMode();
+ }
+
+ /**
+ * Asserts that this file contains exactly the given set of descendants.
+ */
+ public TestFile assertHasDescendants(String... descendants) {
+ Set<String> actual = new TreeSet<String>();
+ assertIsDir();
+ visit(actual, "", this);
+ Set<String> expected = new TreeSet<String>(Arrays.asList(descendants));
+
+ Set<String> extras = new TreeSet<String>(actual);
+ extras.removeAll(expected);
+ Set<String> missing = new TreeSet<String>(expected);
+ missing.removeAll(actual);
+
+ assertEquals(String.format("For dir: %s, extra files: %s, missing files: %s, expected: %s", this, extras, missing, expected), expected, actual);
+
+ return this;
+ }
+
+ public TestFile assertIsEmptyDir() {
+ if (exists()) {
+ assertIsDir();
+ assertHasDescendants();
+ }
+ return this;
+ }
+
+ private void visit(Set<String> names, String prefix, File file) {
+ for (File child : file.listFiles()) {
+ if (child.isFile()) {
+ names.add(prefix + child.getName());
+ } else if (child.isDirectory()) {
+ visit(names, prefix + child.getName() + "/", child);
+ }
+ }
+ }
+
+ public boolean isSelfOrDescendent(File file) {
+ if (file.getAbsolutePath().equals(getAbsolutePath())) {
+ return true;
+ }
+ return file.getAbsolutePath().startsWith(getAbsolutePath() + File.separatorChar);
+ }
+
+ public TestFile createDir() {
+ if (mkdirs()) {
+ return this;
+ }
+ if (isDirectory()) {
+ return this;
+ }
+ throw new AssertionError("Problems creating dir: " + this
+ + ". Diagnostics: exists=" + this.exists() + ", isFile=" + this.isFile() + ", isDirectory=" + this.isDirectory());
+ }
+
+ public TestFile createDir(Object path) {
+ return new TestFile(this, path).createDir();
+ }
+
+ public TestFile deleteDir() {
+ new TestFileHelper(this).delete(useNativeTools);
+ return this;
+ }
+
+ /**
+ * Attempts to delete this directory, ignoring failures to do so.
+ * @return this
+ */
+ public TestFile maybeDeleteDir() {
+ try {
+ deleteDir();
+ } catch (RuntimeException e) {
+ // Ignore
+ }
+ return this;
+ }
+
+ public TestFile createFile() {
+ new TestFile(getParentFile()).createDir();
+ try {
+ assertTrue(isFile() || createNewFile());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return this;
+ }
+
+ public TestFile createFile(Object path) {
+ return file(path).createFile();
+ }
+
+ public TestFile createZip(Object path) {
+ Zip zip = new Zip();
+ zip.setWhenempty((Zip.WhenEmpty) Zip.WhenEmpty.getInstance(Zip.WhenEmpty.class, "create"));
+ TestFile zipFile = file(path);
+ zip.setDestFile(zipFile);
+ zip.setBasedir(this);
+ zip.setExcludes("**");
+ execute(zip);
+ return zipFile;
+ }
+
+ public TestFile zipTo(TestFile zipFile){
+ new TestFileHelper(this).zipTo(zipFile, useNativeTools);
+ return this;
+ }
+
+ public TestFile tarTo(TestFile tarFile) {
+ new TestFileHelper(this).tarTo(tarFile, useNativeTools);
+ return this;
+ }
+
+ public TestFile tgzTo(TestFile tarFile) {
+ Tar tar = new Tar();
+ tar.setBasedir(this);
+ tar.setDestFile(tarFile);
+ tar.setCompression((Tar.TarCompressionMethod) EnumeratedAttribute.getInstance(Tar.TarCompressionMethod.class, "gzip"));
+ execute(tar);
+ return this;
+ }
+
+ public TestFile tbzTo(TestFile tarFile) {
+ Tar tar = new Tar();
+ tar.setBasedir(this);
+ tar.setDestFile(tarFile);
+ tar.setCompression((Tar.TarCompressionMethod) EnumeratedAttribute.getInstance(Tar.TarCompressionMethod.class, "bzip2"));
+ execute(tar);
+ return this;
+ }
+
+ private void execute(Task task) {
+ task.setProject(new Project());
+ task.execute();
+ }
+
+ public Snapshot snapshot() {
+ assertIsFile();
+ return new Snapshot(lastModified(), getHash("MD5"));
+ }
+
+ public void assertHasChangedSince(Snapshot snapshot) {
+ Snapshot now = snapshot();
+ assertTrue(now.modTime != snapshot.modTime || !Arrays.equals(now.hash, snapshot.hash));
+ }
+
+ public void assertContentsHaveChangedSince(Snapshot snapshot) {
+ Snapshot now = snapshot();
+ assertTrue(String.format("contents of %s have not changed", this), !Arrays.equals(now.hash, snapshot.hash));
+ }
+
+ public void assertContentsHaveNotChangedSince(Snapshot snapshot) {
+ Snapshot now = snapshot();
+ assertArrayEquals(String.format("contents of %s has changed", this), snapshot.hash, now.hash);
+ }
+
+ public void assertHasNotChangedSince(Snapshot snapshot) {
+ Snapshot now = snapshot();
+ assertEquals(String.format("last modified time of %s has changed", this), snapshot.modTime, now.modTime);
+ assertArrayEquals(String.format("contents of %s has changed", this), snapshot.hash, now.hash);
+ }
+
+ public void writeProperties(Map<?, ?> properties) {
+ Properties props = new Properties();
+ props.putAll(properties);
+ try {
+ FileOutputStream stream = new FileOutputStream(this);
+ try {
+ props.store(stream, "comment");
+ } finally {
+ stream.close();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public ExecOutput exec(Object... args) {
+ return new TestFileHelper(this).execute(Arrays.asList(args), null);
+ }
+
+ public ExecOutput execute(List args, List env) {
+ return new TestFileHelper(this).execute(args, env);
+ }
+
+ public class Snapshot {
+ private final long modTime;
+ private final byte[] hash;
+
+ public Snapshot(long modTime, byte[] hash) {
+ this.modTime = modTime;
+ this.hash = hash;
+ }
+ }
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/file/TestFileHelper.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/file/TestFileHelper.groovy
new file mode 100644
index 0000000..89e2b41
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/file/TestFileHelper.groovy
@@ -0,0 +1,203 @@
+package com.github.jengelman.gradle.plugins.shadow.util.file;
+
+import org.apache.commons.io.FileUtils
+import org.apache.commons.lang.StringUtils
+import org.apache.tools.ant.Project
+import org.apache.tools.ant.taskdefs.Expand
+import org.apache.tools.ant.taskdefs.Tar
+import org.apache.tools.ant.taskdefs.Untar
+import org.apache.tools.ant.taskdefs.Zip
+
+import java.util.zip.ZipInputStream
+
+import static org.junit.Assert.assertTrue
+
+class TestFileHelper {
+ TestFile file
+
+ TestFileHelper(TestFile file) {
+ this.file = file
+ }
+
+ void unzipTo(File target, boolean nativeTools) {
+ // Check that each directory in hierarchy is present
+ file.withInputStream {InputStream instr ->
+ def dirs = [] as Set
+ def zipStr = new ZipInputStream(instr)
+ def entry
+ while (entry = zipStr.getNextEntry()) {
+ if (entry.directory) {
+ assertTrue("Duplicate directory '$entry.name'", dirs.add(entry.name))
+ }
+ if (!entry.name.contains('/')) {
+ continue
+ }
+ def parent = StringUtils.substringBeforeLast(entry.name, '/') + '/'
+ assertTrue("Missing dir '$parent'", dirs.contains(parent))
+ }
+ }
+
+ if (nativeTools && isUnix()) {
+ def process = ['unzip', '-o', file.absolutePath, '-d', target.absolutePath].execute()
+ process.consumeProcessOutput(System.out, System.err)
+ assert process.waitFor() == 0
+ return
+ }
+
+ def unzip = new Expand()
+ unzip.src = file
+ unzip.dest = target
+
+ unzip.project = new Project()
+ unzip.execute()
+ }
+
+ void untarTo(File target, boolean nativeTools) {
+ if (nativeTools && isUnix()) {
+ target.mkdirs()
+ def builder = new ProcessBuilder(['tar', '-xpf', file.absolutePath])
+ builder.directory(target)
+ def process = builder.start()
+ process.consumeProcessOutput()
+ assert process.waitFor() == 0
+ return
+ }
+
+ def untar = new Untar()
+ untar.setSrc(file)
+ untar.setDest(target)
+
+ if (file.name.endsWith(".tgz")) {
+ def method = new Untar.UntarCompressionMethod()
+ method.value = "gzip"
+ untar.compression = method
+ } else if (file.name.endsWith(".tbz2")) {
+ def method = new Untar.UntarCompressionMethod()
+ method.value = "bzip2"
+ untar.compression = method
+ }
+
+ untar.project = new Project()
+ untar.execute()
+ }
+
+ private boolean isUnix() {
+ return !System.getProperty('os.name').toLowerCase().contains('windows')
+ }
+
+ String getPermissions() {
+ if (!isUnix()) {
+ return "-rwxr-xr-x"
+ }
+
+ def process = ["ls", "-ld", file.absolutePath].execute()
+ def result = process.inputStream.text
+ def error = process.errorStream.text
+ def retval = process.waitFor()
+ if (retval != 0) {
+ throw new RuntimeException("Could not list permissions for '$file': $error")
+ }
+ def perms = result.split()[0]
+ assert perms.matches("[d\\-][rwx\\-]{9}[@\\+]?")
+ return perms.substring(1, 10)
+ }
+
+ void setPermissions(String permissions) {
+ if (!isUnix()) {
+ return
+ }
+ int m = toMode(permissions)
+ setMode(m)
+ }
+
+ void setMode(int mode) {
+ def process = ["chmod", Integer.toOctalString(mode), file.absolutePath].execute()
+ def error = process.errorStream.text
+ def retval = process.waitFor()
+ if (retval != 0) {
+ throw new RuntimeException("Could not set permissions for '$file': $error")
+ }
+ }
+
+ private int toMode(String permissions) {
+ int m = [6, 3, 0].inject(0) { mode, pos ->
+ mode |= permissions[9 - pos - 3] == 'r' ? 4 << pos : 0
+ mode |= permissions[9 - pos - 2] == 'w' ? 2 << pos : 0
+ mode |= permissions[9 - pos - 1] == 'x' ? 1 << pos : 0
+ return mode
+ }
+ return m
+ }
+
+ int getMode() {
+ return toMode(getPermissions())
+ }
+
+ void delete(boolean nativeTools) {
+ if (isUnix() && nativeTools) {
+ def process = ["rm", "-rf", file.absolutePath].execute()
+ def error = process.errorStream.text
+ def retval = process.waitFor()
+ if (retval != 0) {
+ throw new RuntimeException("Could not delete '$file': $error")
+ }
+ } else {
+ FileUtils.deleteQuietly(file);
+ }
+ }
+
+ String readLink() {
+ def process = ["readlink", file.absolutePath].execute()
+ def error = process.errorStream.text
+ def retval = process.waitFor()
+ if (retval != 0) {
+ throw new RuntimeException("Could not read link '$file': $error")
+ }
+ return process.inputStream.text.trim()
+ }
+
+ ExecOutput exec(List args) {
+ return execute(args, null)
+ }
+
+ ExecOutput execute(List args, List env) {
+ def process = ([file.absolutePath] + args).execute(env, null)
+ String output = process.inputStream.text
+ String error = process.errorStream.text
+ if (process.waitFor() != 0) {
+ throw new RuntimeException("Could not execute $file. Error: $error, Output: $output")
+ }
+ return new ExecOutput(output, error)
+ }
+
+ public void zipTo(TestFile zipFile, boolean nativeTools) {
+ if (nativeTools && isUnix()) {
+ def process = ['zip', zipFile.absolutePath, "-r", file.name].execute(null, zipFile.parentFile)
+ process.consumeProcessOutput(System.out, System.err)
+ assert process.waitFor() == 0
+ } else {
+ Zip zip = new Zip();
+ zip.setBasedir(file);
+ zip.setDestFile(zipFile);
+ zip.setProject(new Project());
+ def whenEmpty = new Zip.WhenEmpty()
+ whenEmpty.setValue("create")
+ zip.setWhenempty(whenEmpty);
+ zip.execute();
+ }
+ }
+
+ public void tarTo(TestFile tarFile, boolean nativeTools) {
+ if (nativeTools && isUnix()) {
+ def process = ['tar', "-cf", tarFile.absolutePath, file.name].execute(null, tarFile.parentFile)
+ process.consumeProcessOutput(System.out, System.err)
+ assert process.waitFor() == 0
+ } else {
+ Tar tar = new Tar();
+ tar.setBasedir(file);
+ tar.setDestFile(tarFile);
+ tar.setProject(new Project())
+ tar.execute()
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/file/TestNameTestDirectoryProvider.java b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/file/TestNameTestDirectoryProvider.java
new file mode 100644
index 0000000..a5866ae
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/file/TestNameTestDirectoryProvider.java
@@ -0,0 +1,114 @@
+package com.github.jengelman.gradle.plugins.shadow.util.file;
+
+
+import org.apache.commons.lang.StringUtils;
+import org.junit.rules.MethodRule;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.Statement;
+
+import java.io.File;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A JUnit rule which provides a unique temporary folder for the test.
+ */
+public class TestNameTestDirectoryProvider implements MethodRule, TestRule, TestDirectoryProvider {
+ private TestFile dir;
+ private String prefix;
+ private static TestFile root;
+ private static AtomicInteger testCounter = new AtomicInteger(1);
+
+ static {
+ // NOTE: the space in the directory name is intentional
+ root = new TestFile(new File("build/tmp/test files"));
+ }
+
+ private String determinePrefix() {
+ StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
+ for (StackTraceElement element : stackTrace) {
+ if (element.getClassName().endsWith("Test") || element.getClassName().endsWith("Spec")) {
+ return StringUtils.substringAfterLast(element.getClassName(), ".") + "/unknown-test-" + testCounter.getAndIncrement();
+ }
+ }
+ return "unknown-test-class-" + testCounter.getAndIncrement();
+ }
+
+ public Statement apply(final Statement base, final FrameworkMethod method, final Object target) {
+ init(method.getName(), target.getClass().getSimpleName());
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ base.evaluate();
+ getTestDirectory().maybeDeleteDir();
+ // Don't delete on failure
+ }
+ };
+ }
+
+ public Statement apply(final Statement base, Description description) {
+ init(description.getMethodName(), description.getTestClass().getSimpleName());
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ base.evaluate();
+ getTestDirectory().maybeDeleteDir();
+ // Don't delete on failure
+ }
+ };
+ }
+
+ private void init(String methodName, String className) {
+ if (methodName == null) {
+ // must be a @ClassRule; use the rule's class name instead
+ methodName = getClass().getSimpleName();
+ }
+ if (prefix == null) {
+ String safeMethodName = methodName.replaceAll("\\s", "_").replace(File.pathSeparator, "_").replace(":", "_");
+ if (safeMethodName.length() > 64) {
+ safeMethodName = safeMethodName.substring(0, 32) + "..." + safeMethodName.substring(safeMethodName.length() - 32);
+ }
+ prefix = String.format("%s/%s", className, safeMethodName);
+ }
+ }
+
+ public static TestNameTestDirectoryProvider newInstance() {
+ return new TestNameTestDirectoryProvider();
+ }
+
+ public static TestNameTestDirectoryProvider newInstance(FrameworkMethod method, Object target) {
+ TestNameTestDirectoryProvider testDirectoryProvider = new TestNameTestDirectoryProvider();
+ testDirectoryProvider.init(method.getName(), target.getClass().getSimpleName());
+ return testDirectoryProvider;
+ }
+
+ public TestFile getTestDirectory() {
+ if (dir == null) {
+ if (prefix == null) {
+ // This can happen if this is used in a constructor or a @Before method. It also happens when using
+ // @RunWith(SomeRunner) when the runner does not support rules.
+ prefix = determinePrefix();
+ }
+ for (int counter = 1; true; counter++) {
+ dir = root.file(counter == 1 ? prefix : String.format("%s%d", prefix, counter));
+ if (dir.mkdirs()) {
+ break;
+ }
+ }
+ }
+ return dir;
+ }
+
+ public TestFile file(Object... path) {
+ return getTestDirectory().file((Object[]) path);
+ }
+
+ public TestFile createFile(Object... path) {
+ return file((Object[]) path).createFile();
+ }
+
+ public TestFile createDir(Object... path) {
+ return file((Object[]) path).createDir();
+ }
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/file/TestWorkspaceBuilder.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/file/TestWorkspaceBuilder.groovy
new file mode 100644
index 0000000..ef54d93
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/file/TestWorkspaceBuilder.groovy
@@ -0,0 +1,39 @@
+package com.github.jengelman.gradle.plugins.shadow.util.file
+
+/**
+ * Used in TestFile.create().
+ *
+ * Should be inner class of TestFile, but can't because Groovy has issues with inner classes as delegates.
+ */
+class TestWorkspaceBuilder {
+ def TestFile baseDir
+
+ def TestWorkspaceBuilder(TestFile baseDir) {
+ this.baseDir = baseDir
+ }
+
+ def apply(Closure cl) {
+ cl.delegate = this
+ cl.resolveStrategy = Closure.DELEGATE_FIRST
+ cl()
+ }
+
+ def file(String name) {
+ TestFile file = baseDir.file(name)
+ file.write('some content')
+ file
+ }
+
+ def setMode(int mode) {
+ baseDir.mode = mode
+ }
+
+ def methodMissing(String name, Object args) {
+ if (args.length == 1 && args[0] instanceof Closure) {
+ baseDir.file(name).create(args[0])
+ }
+ else {
+ throw new MissingMethodException(name, getClass(), args)
+ }
+ }
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/AbstractModule.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/AbstractModule.groovy
new file mode 100644
index 0000000..4b9be7c
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/AbstractModule.groovy
@@ -0,0 +1,81 @@
+package com.github.jengelman.gradle.plugins.shadow.util.repo
+
+import com.github.jengelman.gradle.plugins.shadow.util.file.TestFile
+import org.gradle.internal.hash.HashUtil
+
+
+abstract class AbstractModule {
+ /**
+ * @param cl A closure that is passed a writer to use to generate the content.
+ */
+ protected void publish(TestFile file, Closure cl) {
+ def hashBefore = file.exists() ? getHash(file, "sha1") : null
+ def tmpFile = file.parentFile.file("${file.name}.tmp")
+
+ tmpFile.withWriter("utf-8") {
+ cl.call(it)
+ }
+
+ def hashAfter = getHash(tmpFile, "sha1")
+ if (hashAfter == hashBefore) {
+ // Already published
+ return
+ }
+
+ assert !file.exists() || file.delete()
+ assert tmpFile.renameTo(file)
+ onPublish(file)
+ }
+
+ protected void publishWithStream(TestFile file, Closure cl) {
+ def hashBefore = file.exists() ? getHash(file, "sha1") : null
+ def tmpFile = file.parentFile.file("${file.name}.tmp")
+
+ tmpFile.withOutputStream {
+ cl.call(it)
+ }
+
+ def hashAfter = getHash(tmpFile, "sha1")
+ if (hashAfter == hashBefore) {
+ // Already published
+ return
+ }
+
+ assert !file.exists() || file.delete()
+ assert tmpFile.renameTo(file)
+ onPublish(file)
+ }
+
+ protected abstract onPublish(TestFile file)
+
+ TestFile getSha1File(TestFile file) {
+ getHashFile(file, "sha1")
+ }
+
+ TestFile sha1File(TestFile file) {
+ hashFile(file, "sha1", 40)
+ }
+
+ TestFile getMd5File(TestFile file) {
+ getHashFile(file, "md5")
+ }
+
+ TestFile md5File(TestFile file) {
+ hashFile(file, "md5", 32)
+ }
+
+ private TestFile hashFile(TestFile file, String algorithm, int len) {
+ def hashFile = getHashFile(file, algorithm)
+ def hash = getHash(file, algorithm)
+ hashFile.text = String.format("%0${len}x", hash)
+ return hashFile
+ }
+
+ private TestFile getHashFile(TestFile file, String algorithm) {
+ file.parentFile.file("${file.name}.${algorithm}")
+ }
+
+ protected BigInteger getHash(TestFile file, String algorithm) {
+ HashUtil.createHash(file, algorithm.toUpperCase()).asBigInteger()
+ }
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/AbstractMavenModule.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/AbstractMavenModule.groovy
new file mode 100644
index 0000000..8377550
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/AbstractMavenModule.groovy
@@ -0,0 +1,330 @@
+package com.github.jengelman.gradle.plugins.shadow.util.repo.maven
+
+import com.github.jengelman.gradle.plugins.shadow.util.file.TestFile
+import com.github.jengelman.gradle.plugins.shadow.util.repo.AbstractModule
+import groovy.xml.MarkupBuilder
+import java.text.SimpleDateFormat
+
+abstract class AbstractMavenModule extends AbstractModule implements MavenModule {
+ protected static final String MAVEN_METADATA_FILE = "maven-metadata.xml"
+ final TestFile moduleDir
+ final String groupId
+ final String artifactId
+ final String version
+ String parentPomSection
+ String type = 'jar'
+ String packaging
+ int publishCount = 1
+ private final List dependencies = []
+ private final List artifacts = []
+ final updateFormat = new SimpleDateFormat("yyyyMMddHHmmss")
+ final timestampFormat = new SimpleDateFormat("yyyyMMdd.HHmmss")
+
+ AbstractMavenModule(TestFile moduleDir, String groupId, String artifactId, String version) {
+ this.moduleDir = moduleDir
+ this.groupId = groupId
+ this.artifactId = artifactId
+ this.version = version
+ }
+
+ MavenModule parent(String group, String artifactId, String version) {
+ parentPomSection = """
+<parent>
+ <groupId>${group}</groupId>
+ <artifactId>${artifactId}</artifactId>
+ <version>${version}</version>
+</parent>
+"""
+ return this
+ }
+
+ TestFile getArtifactFile(Map options = [:]) {
+ if (version.endsWith("-SNAPSHOT") && !metaDataFile.exists() && uniqueSnapshots) {
+ def artifact = toArtifact(options)
+ return moduleDir.file("${artifactId}-${version}${artifact.classifier ? "-${artifact.classifier}" : ""}.${artifact.type}")
+ }
+ return artifactFile(options)
+ }
+
+ abstract boolean getUniqueSnapshots()
+
+ String getPublishArtifactVersion() {
+ if (uniqueSnapshots && version.endsWith("-SNAPSHOT")) {
+ return "${version.replaceFirst('-SNAPSHOT$', '')}-${getUniqueSnapshotVersion()}"
+ }
+ return version
+ }
+
+ private String getUniqueSnapshotVersion() {
+ assert uniqueSnapshots && version.endsWith('-SNAPSHOT')
+ if (metaDataFile.isFile()) {
+ def metaData = new XmlParser().parse(metaDataFile.assertIsFile())
+ def timestamp = metaData.versioning.snapshot.timestamp[0].text().trim()
+ def build = metaData.versioning.snapshot.buildNumber[0].text().trim()
+ return "${timestamp}-${build}"
+ }
+ return "${timestampFormat.format(publishTimestamp)}-${publishCount}"
+ }
+
+ MavenModule dependsOn(String... dependencyArtifactIds) {
+ for (String id : dependencyArtifactIds) {
+ dependsOn(groupId, id, '1.0')
+ }
+ return this
+ }
+
+ MavenModule dependsOn(String group, String artifactId, String version, String type = null) {
+ this.dependencies << [groupId: group, artifactId: artifactId, version: version, type: type]
+ return this
+ }
+
+ MavenModule hasPackaging(String packaging) {
+ this.packaging = packaging
+ return this
+ }
+
+ /**
+ * Specifies the type of the main artifact.
+ */
+ MavenModule hasType(String type) {
+ this.type = type
+ return this
+ }
+
+ /**
+ * Adds an additional artifact to this module.
+ * @param options Can specify any of: type or classifier
+ */
+ MavenModule artifact(Map<String, ?> options) {
+ artifacts << options
+ return this
+ }
+
+ String getPackaging() {
+ return packaging
+ }
+
+ List getDependencies() {
+ return dependencies
+ }
+
+ List getArtifacts() {
+ return artifacts
+ }
+
+ void assertNotPublished() {
+ pomFile.assertDoesNotExist()
+ }
+
+ void assertPublished() {
+ assert pomFile.assertExists()
+ assert parsedPom.groupId == groupId
+ assert parsedPom.artifactId == artifactId
+ assert parsedPom.version == version
+ }
+
+ void assertPublishedAsPomModule() {
+ assertPublished()
+ assertArtifactsPublished("${artifactId}-${publishArtifactVersion}.pom")
+ assert parsedPom.packaging == "pom"
+ }
+
+ void assertPublishedAsJavaModule() {
+ assertPublished()
+ assertArtifactsPublished("${artifactId}-${publishArtifactVersion}.jar", "${artifactId}-${publishArtifactVersion}.pom")
+ assert parsedPom.packaging == null
+ }
+
+ void assertPublishedAsWebModule() {
+ assertPublished()
+ assertArtifactsPublished("${artifactId}-${publishArtifactVersion}.war", "${artifactId}-${publishArtifactVersion}.pom")
+ assert parsedPom.packaging == 'war'
+ }
+
+ void assertPublishedAsEarModule() {
+ assertPublished()
+ assertArtifactsPublished("${artifactId}-${publishArtifactVersion}.ear", "${artifactId}-${publishArtifactVersion}.pom")
+ assert parsedPom.packaging == 'ear'
+ }
+
+ /**
+ * Asserts that exactly the given artifacts have been deployed, along with their checksum files
+ */
+ void assertArtifactsPublished(String... names) {
+ def artifactNames = names as Set
+ if (publishesMetaDataFile()) {
+ artifactNames.add(MAVEN_METADATA_FILE)
+ }
+ assert moduleDir.isDirectory()
+ Set actual = moduleDir.list() as Set
+ for (name in artifactNames) {
+ assert actual.remove(name)
+
+ if(publishesHashFiles()) {
+ assert actual.remove("${name}.md5" as String)
+ assert actual.remove("${name}.sha1" as String)
+ }
+ }
+ assert actual.isEmpty()
+ }
+
+ //abstract String getPublishArtifactVersion()
+
+ MavenPom getParsedPom() {
+ return new MavenPom(pomFile)
+ }
+
+ DefaultMavenMetaData getRootMetaData() {
+ new DefaultMavenMetaData(rootMetaDataFile)
+ }
+
+ TestFile getPomFile() {
+ return moduleDir.file("$artifactId-${publishArtifactVersion}.pom")
+ }
+
+ TestFile getMetaDataFile() {
+ moduleDir.file(MAVEN_METADATA_FILE)
+ }
+
+ TestFile getRootMetaDataFile() {
+ moduleDir.parentFile.file(MAVEN_METADATA_FILE)
+ }
+
+ TestFile artifactFile(Map<String, ?> options) {
+ def artifact = toArtifact(options)
+ def fileName = "$artifactId-${publishArtifactVersion}.${artifact.type}"
+ if (artifact.classifier) {
+ fileName = "$artifactId-$publishArtifactVersion-${artifact.classifier}.${artifact.type}"
+ }
+ return moduleDir.file(fileName)
+ }
+
+ MavenModule publishWithChangedContent() {
+ publishCount++
+ return publish()
+ }
+
+ protected Map<String, Object> toArtifact(Map<String, ?> options) {
+ options = new HashMap<String, Object>(options)
+ def artifact = [type: options.remove('type') ?: type, classifier: options.remove('classifier') ?: null]
+ assert options.isEmpty(): "Unknown options : ${options.keySet()}"
+ return artifact
+ }
+
+ Date getPublishTimestamp() {
+ return new Date(updateFormat.parse("20100101120000").time + publishCount * 1000)
+ }
+
+ MavenModule publishPom() {
+ moduleDir.createDir()
+ def rootMavenMetaData = getRootMetaDataFile()
+
+ updateRootMavenMetaData(rootMavenMetaData)
+
+ if (publishesMetaDataFile()) {
+ publish(metaDataFile) { Writer writer ->
+ writer << getMetaDataFileContent()
+ }
+ }
+
+ publish(pomFile) { Writer writer ->
+ def pomPackaging = packaging ?: type;
+ writer << """
+<project xmlns="http://maven.apache.org/POM/4.0.0">
+ <!-- ${getArtifactContent()} -->
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>$groupId</groupId>
+ <artifactId>$artifactId</artifactId>
+ <packaging>$pomPackaging</packaging>
+ <version>$version</version>
+ <description>Published on $publishTimestamp</description>"""
+
+ if (parentPomSection) {
+ writer << "\n$parentPomSection\n"
+ }
+
+ if (!dependencies.empty) {
+ writer << """
+ <dependencies>"""
+ }
+
+ dependencies.each { dependency ->
+ def typeAttribute = dependency['type'] == null ? "" : "<type>$dependency.type</type>"
+ writer << """
+ <dependency>
+ <groupId>$dependency.groupId</groupId>
+ <artifactId>$dependency.artifactId</artifactId>
+ <version>$dependency.version</version>
+ $typeAttribute
+ </dependency>"""
+ }
+
+ if (!dependencies.empty) {
+ writer << """
+ </dependencies>"""
+ }
+
+ writer << "\n</project>"
+ }
+ return this
+ }
+
+ private void updateRootMavenMetaData(TestFile rootMavenMetaData) {
+ def allVersions = rootMavenMetaData.exists() ? new XmlParser().parseText(rootMavenMetaData.text).versioning.versions.version*.value().flatten() : []
+ allVersions << version;
+ publish(rootMavenMetaData) { Writer writer ->
+ def builder = new MarkupBuilder(writer)
+ builder.metadata {
+ groupId(groupId)
+ artifactId(artifactId)
+ version(allVersions.max())
+ versioning {
+ if (uniqueSnapshots && version.endsWith("-SNAPSHOT")) {
+ snapshot {
+ timestamp(timestampFormat.format(publishTimestamp))
+ buildNumber(publishCount)
+ lastUpdated(updateFormat.format(publishTimestamp))
+ }
+ } else {
+ versions {
+ allVersions.each {currVersion ->
+ version(currVersion)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ abstract String getMetaDataFileContent()
+
+ MavenModule publish() {
+
+ publishPom()
+ artifacts.each { artifact ->
+ publishArtifact(artifact)
+ }
+ publishArtifact([:])
+ return this
+ }
+
+ File publishArtifact(Map<String, ?> artifact) {
+ def artifactFile = artifactFile(artifact)
+ if (type == 'pom') {
+ return artifactFile
+ }
+ publish(artifactFile) { Writer writer ->
+ writer << "${artifactFile.name} : $artifactContent"
+ }
+ return artifactFile
+ }
+
+ protected String getArtifactContent() {
+ // Some content to include in each artifact, so that its size and content varies on each publish
+ return (0..publishCount).join("-")
+ }
+
+ protected abstract boolean publishesMetaDataFile()
+ protected abstract boolean publishesHashFiles()
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/DefaultMavenMetaData.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/DefaultMavenMetaData.groovy
new file mode 100644
index 0000000..87a317d
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/DefaultMavenMetaData.groovy
@@ -0,0 +1,33 @@
+package com.github.jengelman.gradle.plugins.shadow.util.repo.maven
+
+/**
+ * http://maven.apache.org/ref/3.0.1/maven-repository-metadata/repository-metadata.html
+ */
+class DefaultMavenMetaData implements MavenMetaData{
+
+ String text
+
+ String groupId
+ String artifactId
+ String version
+
+ List<String> versions = []
+ String lastUpdated
+
+ DefaultMavenMetaData(File file) {
+ text = file.text
+ def xml = new XmlParser().parseText(text)
+
+ groupId = xml.groupId[0]?.text()
+ artifactId = xml.artifactId[0]?.text()
+ version = xml.version[0]?.text()
+
+ def versioning = xml.versioning[0]
+
+ lastUpdated = versioning.lastUpdated[0]?.text()
+
+ versioning.versions[0].version.each {
+ versions << it.text()
+ }
+ }
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/MavenDependency.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/MavenDependency.groovy
new file mode 100644
index 0000000..7892513
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/MavenDependency.groovy
@@ -0,0 +1,19 @@
+package com.github.jengelman.gradle.plugins.shadow.util.repo.maven
+
+class MavenDependency {
+ String groupId
+ String artifactId
+ String version
+ String classifier
+ String type
+
+ MavenDependency hasType(def type) {
+ assert this.type == type
+ return this
+ }
+
+ @Override
+ public String toString() {
+ return String.format("MavenDependency %s:%s:%s:%s@%s", groupId, artifactId, version, classifier, type)
+ }
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/MavenFileModule.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/MavenFileModule.groovy
new file mode 100644
index 0000000..7696ee1
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/MavenFileModule.groovy
@@ -0,0 +1,55 @@
+package com.github.jengelman.gradle.plugins.shadow.util.repo.maven
+
+import com.github.jengelman.gradle.plugins.shadow.util.file.TestFile
+
+class MavenFileModule extends AbstractMavenModule {
+ private boolean uniqueSnapshots = true;
+
+ MavenFileModule(TestFile moduleDir, String groupId, String artifactId, String version) {
+ super(moduleDir, groupId, artifactId, version)
+ }
+
+ boolean getUniqueSnapshots() {
+ return uniqueSnapshots
+ }
+
+ MavenModule withNonUniqueSnapshots() {
+ uniqueSnapshots = false;
+ return this;
+ }
+
+ @Override
+ String getMetaDataFileContent() {
+ """
+<metadata>
+ <!-- ${getArtifactContent()} -->
+ <groupId>$groupId</groupId>
+ <artifactId>$artifactId</artifactId>
+ <version>$version</version>
+ <versioning>
+ <snapshot>
+ <timestamp>${timestampFormat.format(publishTimestamp)}</timestamp>
+ <buildNumber>$publishCount</buildNumber>
+ </snapshot>
+ <lastUpdated>${updateFormat.format(publishTimestamp)}</lastUpdated>
+ </versioning>
+</metadata>
+"""
+ }
+
+ @Override
+ protected onPublish(TestFile file) {
+ sha1File(file)
+ md5File(file)
+ }
+
+ @Override
+ protected boolean publishesMetaDataFile() {
+ uniqueSnapshots && version.endsWith("-SNAPSHOT")
+ }
+
+ @Override
+ protected boolean publishesHashFiles() {
+ true
+ }
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/MavenFileRepository.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/MavenFileRepository.groovy
new file mode 100644
index 0000000..ee526fd
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/MavenFileRepository.groovy
@@ -0,0 +1,23 @@
+package com.github.jengelman.gradle.plugins.shadow.util.repo.maven
+
+import com.github.jengelman.gradle.plugins.shadow.util.file.TestFile
+
+/**
+ * A fixture for dealing with file Maven repositories.
+ */
+class MavenFileRepository implements MavenRepository {
+ final TestFile rootDir
+
+ MavenFileRepository(TestFile rootDir) {
+ this.rootDir = rootDir
+ }
+
+ URI getUri() {
+ return rootDir.toURI()
+ }
+
+ MavenFileModule module(String groupId, String artifactId, Object version = '1.0') {
+ def artifactDir = rootDir.file("${groupId.replace('.', '/')}/$artifactId/$version")
+ return new MavenFileModule(artifactDir, groupId, artifactId, version as String)
+ }
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/MavenMetaData.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/MavenMetaData.groovy
new file mode 100644
index 0000000..bdceb46
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/MavenMetaData.groovy
@@ -0,0 +1,6 @@
+package com.github.jengelman.gradle.plugins.shadow.util.repo.maven
+
+public interface MavenMetaData {
+ List<String> getVersions();
+
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/MavenModule.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/MavenModule.groovy
new file mode 100644
index 0000000..2698e0a
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/MavenModule.groovy
@@ -0,0 +1,45 @@
+package com.github.jengelman.gradle.plugins.shadow.util.repo.maven
+
+import com.github.jengelman.gradle.plugins.shadow.util.file.TestFile
+
+interface MavenModule {
+ /**
+ * Publishes the pom.xml plus main artifact, plus any additional artifacts for this module. Publishes only those artifacts whose content has
+ * changed since the last call to {@code #publish()}.
+ */
+ MavenModule publish()
+
+ /**
+ * Publishes the pom.xml only
+ */
+ MavenModule publishPom()
+
+ /**
+ * Publishes the pom.xml plus main artifact, plus any additional artifacts for this module, with different content (and size) to any
+ * previous publication.
+ */
+ MavenModule publishWithChangedContent()
+
+ MavenModule withNonUniqueSnapshots()
+
+ MavenModule parent(String group, String artifactId, String version)
+
+ MavenModule dependsOn(String group, String artifactId, String version)
+
+ MavenModule hasPackaging(String packaging)
+
+ /**
+ * Sets the type of the main artifact for this module.
+ */
+ MavenModule hasType(String type)
+
+ TestFile getPomFile()
+
+ TestFile getArtifactFile()
+
+ TestFile getMetaDataFile()
+
+ MavenPom getParsedPom()
+
+ MavenMetaData getRootMetaData()
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/MavenPom.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/MavenPom.groovy
new file mode 100644
index 0000000..6dd36c5
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/MavenPom.groovy
@@ -0,0 +1,42 @@
+package com.github.jengelman.gradle.plugins.shadow.util.repo.maven
+
+class MavenPom {
+ String groupId
+ String artifactId
+ String version
+ String packaging
+ String description
+ final Map<String, MavenScope> scopes = [:]
+
+ MavenPom(File pomFile) {
+ if (pomFile.exists()){
+ def pom = new XmlParser().parse(pomFile)
+
+ groupId = pom.groupId[0]?.text()
+ artifactId = pom.artifactId[0]?.text()
+ version = pom.version[0]?.text()
+ packaging = pom.packaging[0]?.text()
+ description = pom.description[0]?.text()
+
+ pom.dependencies.dependency.each { dep ->
+ def scopeElement = dep.scope
+ def scopeName = scopeElement ? scopeElement.text() : "runtime"
+ def scope = scopes[scopeName]
+ if (!scope) {
+ scope = new MavenScope()
+ scopes[scopeName] = scope
+ }
+ MavenDependency mavenDependency = new MavenDependency(
+ groupId: dep.groupId.text(),
+ artifactId: dep.artifactId.text(),
+ version: dep.version.text(),
+ classifier: dep.classifier ? dep.classifier.text() : null,
+ type: dep.type ? dep.type.text() : null
+ )
+ def key = "${mavenDependency.groupId}:${mavenDependency.artifactId}:${mavenDependency.version}"
+ key += mavenDependency.classifier ? ":${mavenDependency.classifier}" : ""
+ scope.dependencies[key] = mavenDependency
+ }
+ }
+ }
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/MavenRepository.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/MavenRepository.groovy
new file mode 100644
index 0000000..27b077c
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/MavenRepository.groovy
@@ -0,0 +1,12 @@
+package com.github.jengelman.gradle.plugins.shadow.util.repo.maven
+
+/**
+ * A fixture for dealing with Maven repositories.
+ */
+interface MavenRepository {
+ URI getUri()
+
+ MavenModule module(String groupId, String artifactId)
+
+ MavenModule module(String groupId, String artifactId, Object version)
+}
diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/MavenScope.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/MavenScope.groovy
new file mode 100644
index 0000000..56883bf
--- /dev/null
+++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/util/repo/maven/MavenScope.groovy
@@ -0,0 +1,29 @@
+package com.github.jengelman.gradle.plugins.shadow.util.repo.maven
+
+import org.apache.commons.lang.StringUtils
+
+class MavenScope {
+ Map<String, MavenDependency> dependencies = [:]
+
+ void assertDependsOn(String[] expected) {
+ assert dependencies.size() == expected.length
+ expected.each {
+ String key = StringUtils.substringBefore(it, "@")
+ def dependency = expectDependency(key)
+
+ String type = null
+ if (it != key) {
+ type = StringUtils.substringAfter(it, "@")
+ }
+ assert dependency.hasType(type)
+ }
+ }
+
+ MavenDependency expectDependency(String key) {
+ final dependency = dependencies[key]
+ if (dependency == null) {
+ throw new AssertionError("Could not find expected dependency $key. Actual: ${dependencies.values()}")
+ }
+ return dependency
+ }
+}
diff --git a/src/test/jars/plexus-utils-1.4.1.jar b/src/test/jars/plexus-utils-1.4.1.jar
new file mode 100644
index 0000000..690fc04
Binary files /dev/null and b/src/test/jars/plexus-utils-1.4.1.jar differ
diff --git a/src/test/jars/test-artifact-1.0-SNAPSHOT.jar b/src/test/jars/test-artifact-1.0-SNAPSHOT.jar
new file mode 100644
index 0000000..009abad
Binary files /dev/null and b/src/test/jars/test-artifact-1.0-SNAPSHOT.jar differ
diff --git a/src/test/jars/test-project-1.0-SNAPSHOT.jar b/src/test/jars/test-project-1.0-SNAPSHOT.jar
new file mode 100644
index 0000000..f80e03f
Binary files /dev/null and b/src/test/jars/test-project-1.0-SNAPSHOT.jar differ
diff --git a/src/test/resources/components-1.xml b/src/test/resources/components-1.xml
new file mode 100644
index 0000000..3523573
--- /dev/null
+++ b/src/test/resources/components-1.xml
@@ -0,0 +1,48 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.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.
+-->
+
+<component-set>
+ <components>
+ <component>
+ <role>org.apache.maven.wagon.Wagon</role>
+ <role-hint>http</role-hint>
+ <implementation>org.apache.maven.wagon.providers.http.LightweightHttpWagon</implementation>
+ <instantiation-strategy>per-lookup</instantiation-strategy>
+ <description>LightweightHttpWagon</description>
+ <isolated-realm>false</isolated-realm>
+ </component>
+ <component>
+ <role>org.apache.maven.wagon.Wagon</role>
+ <role-hint>https</role-hint>
+ <implementation>org.apache.maven.wagon.providers.http.LightweightHttpsWagon</implementation>
+ <instantiation-strategy>per-lookup</instantiation-strategy>
+ <description>LIghtweightHttpsWagon</description>
+ <isolated-realm>false</isolated-realm>
+ <configuration>
+ <httpHeaders>
+ <property>
+ <name>User-Agent</name>
+ <value>Apache Maven/${project.version}</value>
+ </property>
+ </httpHeaders>
+ </configuration>
+ </component>
+ </components>
+</component-set>
+
\ No newline at end of file
diff --git a/src/test/resources/components-2.xml b/src/test/resources/components-2.xml
new file mode 100644
index 0000000..c5eff71
--- /dev/null
+++ b/src/test/resources/components-2.xml
@@ -0,0 +1,48 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.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.
+-->
+
+<component-set>
+ <components>
+ <component>
+ <role>org.apache.maven.wagon.Wagon</role>
+ <role-hint>http</role-hint>
+ <implementation>org.apache.maven.wagon.providers.http.LightweightHttpWagon</implementation>
+ <instantiation-strategy>per-lookup</instantiation-strategy>
+ <description>LightweightHttpWagon</description>
+ <isolated-realm>false</isolated-realm>
+ <configuration>
+ <httpHeaders>
+ <property>
+ <name>User-Agent</name>
+ <value>Apache Maven/${project.version}</value>
+ </property>
+ </httpHeaders>
+ </configuration>
+ </component>
+ <component>
+ <role>org.apache.maven.wagon.Wagon</role>
+ <role-hint>https</role-hint>
+ <implementation>org.apache.maven.wagon.providers.http.LightweightHttpsWagon</implementation>
+ <instantiation-strategy>per-lookup</instantiation-strategy>
+ <description>LIghtweightHttpsWagon</description>
+ <isolated-realm>false</isolated-realm>
+ </component>
+ </components>
+</component-set>
+
\ No newline at end of file
diff --git a/src/test/resources/components-expected.xml b/src/test/resources/components-expected.xml
new file mode 100644
index 0000000..4b69cda
--- /dev/null
+++ b/src/test/resources/components-expected.xml
@@ -0,0 +1,55 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.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.
+-->
+
+<component-set>
+ <components>
+ <component>
+ <role>org.apache.maven.wagon.Wagon</role>
+ <role-hint>http</role-hint>
+ <implementation>org.apache.maven.wagon.providers.http.LightweightHttpWagon</implementation>
+ <instantiation-strategy>per-lookup</instantiation-strategy>
+ <description>LightweightHttpWagon</description>
+ <isolated-realm>false</isolated-realm>
+ <configuration>
+ <httpHeaders>
+ <property>
+ <name>User-Agent</name>
+ <value>Apache Maven/${project.version}</value>
+ </property>
+ </httpHeaders>
+ </configuration>
+ </component>
+ <component>
+ <role>org.apache.maven.wagon.Wagon</role>
+ <role-hint>https</role-hint>
+ <implementation>org.apache.maven.wagon.providers.http.LightweightHttpsWagon</implementation>
+ <instantiation-strategy>per-lookup</instantiation-strategy>
+ <description>LIghtweightHttpsWagon</description>
+ <isolated-realm>false</isolated-realm>
+ <configuration>
+ <httpHeaders>
+ <property>
+ <name>User-Agent</name>
+ <value>Apache Maven/${project.version}</value>
+ </property>
+ </httpHeaders>
+ </configuration>
+ </component>
+ </components>
+</component-set>
\ No newline at end of file
diff --git a/src/test/resources/junit-3.8.2.jar b/src/test/resources/junit-3.8.2.jar
new file mode 100644
index 0000000..c8f711d
Binary files /dev/null and b/src/test/resources/junit-3.8.2.jar differ
diff --git a/src/test/resources/test-artifact-1.0-SNAPSHOT.jar b/src/test/resources/test-artifact-1.0-SNAPSHOT.jar
new file mode 100644
index 0000000..009abad
Binary files /dev/null and b/src/test/resources/test-artifact-1.0-SNAPSHOT.jar differ
diff --git a/src/test/resources/test-project-1.0-SNAPSHOT.jar b/src/test/resources/test-project-1.0-SNAPSHOT.jar
new file mode 100644
index 0000000..f80e03f
Binary files /dev/null and b/src/test/resources/test-project-1.0-SNAPSHOT.jar differ
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-java/gradle-shadow-plugin.git
More information about the pkg-java-commits
mailing list