[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