├── .gitignore ├── .travis.yml ├── CHANGES.txt ├── LICENSE.txt ├── NOTICE.txt ├── README.md ├── build.gradle ├── examples └── gradle │ ├── build.gradle │ ├── config │ └── xml-patches │ │ ├── multi │ │ ├── included-multi-patch.xml │ │ └── resources-multi-patch.xml │ │ └── single │ │ ├── first-patch.xml │ │ └── second-patch.xml │ └── src │ └── main │ ├── example-documents │ ├── dont-patch-me.xml │ ├── patch-me-twice.xml │ └── patch-me.xml │ └── resources │ └── com │ └── github │ └── dnault │ └── xmlpatch │ └── example │ └── patch-me.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src ├── main ├── groovy │ └── com │ │ └── github │ │ └── dnault │ │ └── xmlpatch │ │ └── filter │ │ └── multi │ │ ├── XmlMultiPatch.java │ │ └── XmlPatchSpec.groovy ├── java │ └── com │ │ └── github │ │ └── dnault │ │ └── xmlpatch │ │ ├── BatchPatcher.java │ │ ├── CommandLineDriver.java │ │ ├── ErrorCondition.java │ │ ├── PatchException.java │ │ ├── Patcher.java │ │ ├── ant │ │ ├── AbstractPatchTask.java │ │ ├── AntXmlPatchFilter.java │ │ ├── PatchXmlDir.java │ │ └── PatchXmlFile.java │ │ ├── batch │ │ ├── AssembledPatch.java │ │ └── PatchAssembler.java │ │ ├── filter │ │ ├── XmlPatch.java │ │ └── multi │ │ │ └── XmlPatchOptions.java │ │ └── internal │ │ ├── DeferredInitFilterReader.java │ │ ├── IoHelper.java │ │ ├── Log.java │ │ ├── XmlHelper.java │ │ └── logging │ │ ├── ConsoleLogger.java │ │ ├── Slf4jLogger.java │ │ └── XmlPatchLogger.java └── resources │ ├── META-INF │ └── xml-patch │ │ ├── commons-io-LICENSE.txt │ │ ├── commons-io-NOTICE.txt │ │ ├── jaxen-LICENSE.txt │ │ ├── jdom-LICENSE.txt │ │ ├── jdom-info.xml │ │ └── joptsimple-LICENSE.txt │ └── com │ └── github │ └── dnault │ └── xmlpatch │ └── antlib.xml └── test ├── java └── com │ └── github │ └── dnault │ └── xmlpatch │ ├── filter │ └── multi │ │ └── XmlMultiPatchTest.java │ └── test │ ├── AddAttributeTest.java │ ├── AddContentTest.java │ ├── DataDrivenTest.java │ ├── ReplaceNamespaceTest.java │ ├── Rfc7351Test.java │ ├── TestHelper.java │ └── XmlHelperTest.java └── resources └── com └── github └── dnault └── xmlpatch └── test └── regression-test.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build 3 | out 4 | .DS_Store 5 | .idea 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk8 4 | 5 | before_cache: 6 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 7 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 8 | cache: 9 | directories: 10 | - $HOME/.gradle/caches/ 11 | - $HOME/.gradle/wrapper/ -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | Thank you to the kind folks who have contributed pull requests, bug reports, and ideas for enhancements. 4 | 5 | 6 | Relase 0.3.0: 7 | * Issue #6 Create standalone runnable JAR -- Suggested by Maurizio Pillitu (maoo) 8 | * Issue #5 Switch to jdom2 -- Contributed by Norbert Kiesel (nkiesel) 9 | * Issue #4 Upgraded gradle, gradle plugins, and jars -- Contributed by Norbert Kiesel (nkiesel) 10 | 11 | Relase 0.2.0: 12 | * Issue #2 Allow "patch" root element in accordance with RFC 7351 -- Suggested by Jonathan Buhacoff (jbuhacoff) 13 | 14 | Release 0.1.0: 15 | * Initial release 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | 2 | The standalone distribution of xml-patch includes repackaged libraries developed by third parties. 3 | Here's a big THANK YOU to the developers of: 4 | 5 | ---- 6 | 7 | JDOM 8 | Copyright 2000-2015 Jason Hunter 9 | 10 | LICENSE: JDOM License (BSD-like) 11 | 12 | ---- 13 | 14 | Jaxen 15 | Copyright 2003-2006 The Werken Company. All Rights Reserved. 16 | 17 | LICENSE: BSD 3-Clause 18 | 19 | ---- 20 | 21 | joptsimple 22 | Copyright 2004-2015 Paul R. Holser, Jr. 23 | 24 | LICENSE: MIT License. 25 | 26 | ---- 27 | 28 | Apache Commons IO 29 | Copyright 2002-2012 The Apache Software Foundation 30 | 31 | This product includes software developed by 32 | The Apache Software Foundation (http://www.apache.org/). 33 | 34 | LICENSE: Apache License 2.0 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xml-patch 2 | 3 | [![Build Status](https://travis-ci.org/dnault/xml-patch.svg?branch=main)](https://travis-ci.org/dnault/xml-patch) 4 | [![Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](http://www.apache.org/licenses/LICENSE-2.0) 5 | ![Java 1.7+](https://img.shields.io/badge/java-1.7+-lightgray.svg) 6 | 7 | Java implementation of RFC 5261: An XML Patch Operations Framework Utilizing XPath Selectors 8 | 9 | Also compatible with RFC 7351 patch documents. 10 | 11 | Featuring Gradle and Ant integration. 12 | 13 | # Maven Coordinates 14 | 15 | ```xml 16 | 17 | com.github.dnault 18 | xml-patch 19 | 0.3.1 20 | 21 | ``` 22 | 23 | You can also [download the JAR](https://jcenter.bintray.com/com/github/dnault/xml-patch/0.3.1/xml-patch-0.3.1.jar) 24 | if you want to run the patcher from the command line. 25 | 26 | 27 | ## Introduction 28 | 29 | So you're interested in patching some XML documents? Here's a quick illustration of why RFC 5261 XML patches 30 | are the way to go. 31 | 32 | Original document: 33 | 34 | ```xml 35 | 36 | 37 | Hello sorrow 38 | 39 | ``` 40 | 41 | 42 | RFC 5261 patch: 43 | ```xml 44 | 45 | https://tools.ietf.org/html/rfc5261 46 | Goodbye sorrow 47 | 48 | Hello XML patch 49 | 50 | ``` 51 | 52 | Result of applying the patch to the original document: 53 | ```xml 54 | 55 | 56 | Goodbye sorrow 57 | Hello XML patch 58 | 59 | ``` 60 | 61 | Check out [RFC 5261](https://tools.ietf.org/html/rfc5261) for more examples 62 | and a detailed explanation of the patch language. 63 | 64 | 65 | ## Multi-Patch 66 | 67 | RFC 5261 is great for patching one document at a time. If you need to patch many documents, 68 | it may be convienient to bundle related patches into a 'multi-patch' document that can target 69 | multiple, specific files. 70 | 71 | 72 | Original file 'pets.xml': 73 | 74 | ```xml 75 | 76 | 77 | 78 | 79 | ``` 80 | 81 | Original file 'stores.xml': 82 | 83 | ```xml 84 | 85 | 86 | 87 | ``` 88 | 89 | Multi-patch: 90 | 91 | ```xml 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | ``` 104 | 105 | As you can see, a multi-patch bundles zero or more RFC 5261 `diff` elements into a single document. 106 | Each `diff` element has a `file` attribute specifying a relative path to the file targeted by the patch. 107 | 108 | If your project configuration is spread across multiple XML files (web.xml, log4j.xml, foo.xml, etc.) 109 | then a multi-patch is a convenient way to patch all of these files at once. 110 | 111 | 112 | ### Multi-Patch Composition 113 | 114 | Multi-patches may include other multi-patches. This is useful if your project has multiple configurations 115 | that share some common patches. 116 | 117 | 118 | Multipatch file 'common-multi-patch.xml': 119 | ```xml 120 | 121 | 122 | 123 | 124 | 125 | ``` 126 | 127 | Multipatch file 'testing-multi-patch.xml': 128 | ```xml 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | ``` 137 | 138 | Multipatch file 'production-multi-patch.xml': 139 | ```xml 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | ``` 148 | 149 | ## Using the patch tool 150 | 151 | The xml-patch library may invoked from the command line, 152 | or from build tools like Gradle or Ant. 153 | 154 | 155 | ### Gradle 156 | 157 | XML patching is implemented using Gradle's built-in support for custom filters. 158 | Any task implementing CopySpec can be augmented to perform XML patching. 159 | 160 | File 'build.gradle': 161 | 162 | ```groovy 163 | buildscript { 164 | repositories { 165 | jcenter() 166 | } 167 | 168 | dependencies { 169 | classpath "com.github.dnault:xml-patch:0.2.0" 170 | } 171 | } 172 | 173 | import com.github.dnault.xmlpatch.filter.XmlPatch 174 | import com.github.dnault.xmlpatch.filter.multi.XmlMultiPatch 175 | import com.github.dnault.xmlpatch.filter.multi.XmlPatchSpec 176 | 177 | apply plugin: 'java' 178 | 179 | ext { 180 | patchDir = "config/xml-patches" 181 | } 182 | 183 | // Copy a directory, selectively applying a single-target patch. 184 | // 185 | // Use this single-target technique if you're only patching a few documents 186 | // and don't require multi-patch composition. 187 | // 188 | task singleTargetPatchExample(type: Copy) { 189 | inputs.dir patchDir 190 | 191 | from "src/main/example-documents" 192 | into "${buildDir}/patched" 193 | 194 | // Use one 'filesMatching' statement for each file you want to patch. 195 | 196 | filesMatching("**/patch-me.xml") { 197 | // Single-target patches use the 'XmlPatch' filter. 198 | filter(XmlPatch, patch: "${patchDir}/single/first-patch.xml") 199 | } 200 | 201 | filesMatching("**/patch-me-twice.xml") { 202 | // Multiple filters may be applied in sequence. 203 | filter(XmlPatch, patch: "${patchDir}/single/first-patch.xml") 204 | filter(XmlPatch, patch: "${patchDir}/single/second-patch.xml") 205 | } 206 | } 207 | 208 | 209 | // Augment the standard 'processResources' task to apply a multi-patch 210 | // to classpath resources. The same technique could be applied to the 'war' task 211 | // to patch webapp documents. 212 | // 213 | // Use this multi-patch technique if you're patching many documents or want 214 | // to take advantage of multi-patch composition. 215 | // 216 | processResources { 217 | inputs.dir patchDir 218 | 219 | // To apply a multi-patch, first create a patch specification. 220 | def resourcesPatch = new XmlPatchSpec("${patchDir}/multi/resources-multi-patch.xml") 221 | 222 | filesMatching("**/*.xml") { 223 | // Multi-patches use the 'XMlMultiPatch' filter. 224 | // The filter will only process the file if the 'path' specified here 225 | // matches the 'file' attribute of a in the multi-patch document. 226 | filter(XmlMultiPatch, spec: resourcesPatch, path: it.path) 227 | } 228 | 229 | // Optionally assert that each file targeted by the patch was seen by the filter. 230 | doLast { 231 | resourcesPatch.done() 232 | } 233 | } 234 | ``` 235 | 236 | 237 | ### Command Line 238 | 239 | You'll need the JAR for this. You can grab it from the download link in the 240 | **Maven Coordinates** section above. 241 | 242 | Apply an RFC 5261 patch: 243 | 244 | java -jar xml-patch-.jar 245 | 246 | A dash (-) may be used to indicate standard input / output. 247 | 248 | 249 | # Credits 250 | 251 | This library includes the following software, repackaged to avoid dependency conflicts: 252 | 253 | * JDOM (JDOM License, BSD-like) Copyright 2000-2015 Jason Hunter 254 | * Jaxen (BSD 3-Clause) Copyright 2003-2006 The Werken Company. All Rights Reserved. 255 | * joptsimple (MIT License) Copyright 2004-2015 Paul R. Holser, Jr. 256 | * Apache Commons IO (Apache License 2.0) Copyright 2002-2012 The Apache Software Foundation 257 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "java" 3 | id "groovy" 4 | id "maven-publish" 5 | id "application" 6 | id 'com.github.johnrengelman.plugin-shadow' version '2.0.2' 7 | id "com.jfrog.bintray" version "1.5" 8 | } 9 | 10 | group = "com.github.dnault" 11 | version = "0.3.2-SNAPSHOT" 12 | 13 | repositories { 14 | jcenter() 15 | } 16 | 17 | tasks.withType(Tar) { 18 | compression = Compression.GZIP 19 | } 20 | 21 | targetCompatibility = "1.7" 22 | sourceCompatibility = "1.7" 23 | 24 | mainClassName = "com.github.dnault.xmlpatch.CommandLineDriver" 25 | 26 | jar { 27 | manifest { 28 | attributes("Main-Class": mainClassName) 29 | } 30 | } 31 | 32 | dependencies { 33 | shadow gradleApi() 34 | shadow localGroovy() 35 | compileOnly "org.apache.ant:ant:1.9.6" 36 | 37 | compile "org.jdom:jdom2:2.0.6" 38 | compile "jaxen:jaxen:1.1.6" 39 | compile "commons-io:commons-io:2.4" 40 | compile "net.sf.jopt-simple:jopt-simple:4.9" 41 | 42 | testCompile "junit:junit:4.12" 43 | } 44 | 45 | shadowJar { 46 | classifier = null 47 | 48 | ['org.apache.commons', 'org.jaxen', 'org.jdom2', 'joptsimple'].each { 49 | relocate it, "com.github.dnault.xmlpatch.repackaged.${it}" 50 | } 51 | 52 | // JAXEN-209 Jaxen includes a duplicate, obsolete version of this class not required by modern JDKs 53 | exclude 'org/w3c/dom/UserDataHandler.class' 54 | 55 | exclude 'META-INF/maven/**' 56 | exclude 'META-INF/*.txt' 57 | exclude 'META-INF/*.xml' 58 | } 59 | 60 | task sourceJar(type: Jar, dependsOn: classes) { 61 | from sourceSets.main.allSource 62 | } 63 | 64 | task javadocJar(type: Jar, dependsOn: javadoc) { 65 | from javadoc.destinationDir 66 | } 67 | 68 | def gitUrl = 'git@github.com:dnault/xml-patch.git'; 69 | ext.description = "Java implementation of RFC 5261: An XML Patch Operations Framework Utilizing XPath Selectors" 70 | def pomConfig = { 71 | name "${group}:${project.name}" 72 | description project.ext.description 73 | url 'https://github.com/dnault/xml-patch' 74 | scm { 75 | url "${gitUrl}" 76 | connection "scm:git:${gitUrl}" 77 | developerConnection "scm:git:${gitUrl}" 78 | } 79 | licenses { 80 | license { 81 | name 'Apache License 2.0' 82 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt' 83 | } 84 | } 85 | developers { 86 | developer { 87 | name 'David Nault' 88 | email 'dnault@mac.com' 89 | organization 'dnault' 90 | organizationUrl 'https://github.com/dnault' 91 | } 92 | } 93 | } 94 | 95 | task install { 96 | dependsOn(publishToMavenLocal) 97 | } 98 | 99 | publishing { 100 | publications { 101 | mavenJava(MavenPublication) { publication -> 102 | project.shadow.component(publication) 103 | 104 | pom.withXml { 105 | asNode().appendNode('description', project.ext.description) 106 | asNode().children().last() + pomConfig 107 | } 108 | 109 | artifact sourceJar { 110 | classifier 'sources' 111 | } 112 | 113 | artifact javadocJar { 114 | classifier 'javadoc' 115 | } 116 | } 117 | } 118 | } 119 | 120 | bintrayUpload.doFirst { 121 | if (version.contains("SNAPSHOT")) { 122 | throw new RuntimeException("Must not upload snapshots to bintray (current version is ${version}) -- create and tag a release version first!") 123 | } 124 | } 125 | 126 | bintray { 127 | 128 | if (project.hasProperty('bintrayUsername')) { 129 | user = project.bintrayUsername 130 | } 131 | 132 | if (project.hasProperty('bintrayApiKey')) { 133 | key = project.bintrayApiKey 134 | } 135 | 136 | publications = ['mavenJava'] // When uploading Maven-based publication files 137 | 138 | dryRun = false //false //Whether to run this as dry-run, without deploying 139 | publish = false //true //If version should be auto published after an upload 140 | 141 | pkg { 142 | repo = 'maven' 143 | // userOrg = 'myorg' //An optional organization name when the repo belongs to one of the user's orgs 144 | name = 'xml-patch' 145 | desc = 'Java implementation of RFC 5261: An XML Patch Operations Framework Utilizing XPath Selectors' 146 | websiteUrl = 'https://github.com/dnault/xml-patch' 147 | issueTrackerUrl = 'https://github.com/dnault/xml-patch/issues' 148 | vcsUrl = 'https://github.com/dnault/xml-patch.git' 149 | licenses = ['Apache-2.0'] 150 | labels = ['xml', 'patch'] 151 | publicDownloadNumbers = false//true 152 | //attributes= ['a': ['ay1', 'ay2'], 'b': ['bee'], c: 'cee'] //Optional package-level attributes 153 | //Optional version descriptor 154 | version { 155 | name = project.version //'1.3-Final' //Bintray logical version name 156 | //desc = 'optional, version-specific description' 157 | released = new Date() //'optional, date of the version release' //2 possible values: date in the format of 'yyyy-MM-dd'T'HH:mm:ss.SSSZZ' OR a java.util.Date instance 158 | vcsTag = project.version //'1.3.0' 159 | // attributes = ['gradle-plugin': 'com.use.less:com.use.less.gradle:gradle-useless-plugin'] //Optional version-level attributes 160 | gpg { 161 | // sign = true //Determines whether to GPG sign the files. The default is false 162 | // passphrase = 'passphrase' //Optional. The passphrase for GPG signing' 163 | } 164 | mavenCentralSync { 165 | sync = false //true //Optional (true by default). Determines whether to sync the version to Maven Central. 166 | user = 'userToken' //OSS user token 167 | password = 'paasword' //OSS user password 168 | close = '0'//'1' //Optional property. By default the staging repository is closed and artifacts are released to Maven Central. You can optionally turn this behaviour off (by puting 0 as value) and release the version manually. 169 | } 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /examples/gradle/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | 6 | dependencies { 7 | classpath "com.github.dnault:xml-patch:0.3.1" 8 | } 9 | } 10 | 11 | import com.github.dnault.xmlpatch.filter.XmlPatch 12 | import com.github.dnault.xmlpatch.filter.multi.XmlMultiPatch 13 | import com.github.dnault.xmlpatch.filter.multi.XmlPatchSpec 14 | 15 | apply plugin: 'java' 16 | 17 | ext { 18 | patchDir = "config/xml-patches" 19 | } 20 | 21 | // Copy a directory, selectively applying a single-target patch. 22 | // 23 | // Use this single-target technique if you're only patching a few documents 24 | // and don't require multi-patch composition. 25 | // 26 | task singleTargetPatchExample(type: Copy) { 27 | inputs.dir patchDir 28 | 29 | from "src/main/example-documents" 30 | into "${buildDir}/patched" 31 | 32 | // Use one 'filesMatching' statement for each file you want to patch. 33 | 34 | filesMatching("**/patch-me.xml") { 35 | // Single-target patches use the 'XmlPatch' filter. 36 | filter(XmlPatch, patch: "${patchDir}/single/first-patch.xml") 37 | } 38 | 39 | filesMatching("**/patch-me-twice.xml") { 40 | // Multiple filters may be applied in sequence. 41 | filter(XmlPatch, patch: "${patchDir}/single/first-patch.xml") 42 | filter(XmlPatch, patch: "${patchDir}/single/second-patch.xml") 43 | } 44 | } 45 | 46 | // Augment the standard 'processResources' task to apply a multi-patch 47 | // to classpath resources. The same technique could be applied to the 'war' task 48 | // to patch webapp documents. 49 | // 50 | // Use this multi-patch technique if you're patching many documents or want 51 | // to take advantage of multi-patch composition. 52 | // 53 | processResources { 54 | inputs.dir patchDir 55 | 56 | // To apply a multi-patch, first create a patch specification. 57 | def resourcesPatch = new XmlPatchSpec("${patchDir}/multi/resources-multi-patch.xml") 58 | 59 | filesMatching("**/*.xml") { 60 | // Multi-patches use the 'XMlMultiPatch' filter. 61 | // The filter will only process the file if the 'path' specified here 62 | // matches the 'file' attribute of a in the multi-patch document. 63 | filter(XmlMultiPatch, spec: resourcesPatch, path: it.path) 64 | } 65 | 66 | // Optionally assert that each file targeted by the patch was seen by the filter. 67 | doLast { 68 | resourcesPatch.done() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/gradle/config/xml-patches/multi/included-multi-patch.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /examples/gradle/config/xml-patches/multi/resources-multi-patch.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /examples/gradle/config/xml-patches/single/first-patch.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /examples/gradle/config/xml-patches/single/second-patch.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /examples/gradle/src/main/example-documents/dont-patch-me.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/gradle/src/main/example-documents/patch-me-twice.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/gradle/src/main/example-documents/patch-me.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/gradle/src/main/resources/com/github/dnault/xmlpatch/example/patch-me.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnault/xml-patch/65828e2d4ef5b9ceea013111b632937d2b49cb33/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Dec 08 17:50:13 PST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/dnault/xmlpatch/filter/multi/XmlMultiPatch.java: -------------------------------------------------------------------------------- 1 | package com.github.dnault.xmlpatch.filter.multi; 2 | 3 | import java.io.File; 4 | import java.io.FileOutputStream; 5 | import java.io.IOException; 6 | import java.io.Reader; 7 | 8 | import com.github.dnault.xmlpatch.filter.XmlPatch; 9 | import com.github.dnault.xmlpatch.internal.Log; 10 | import com.github.dnault.xmlpatch.batch.AssembledPatch; 11 | import com.github.dnault.xmlpatch.internal.DeferredInitFilterReader; 12 | import org.apache.commons.io.FileUtils; 13 | import org.jdom2.Element; 14 | import org.jdom2.output.Format; 15 | import org.jdom2.output.XMLOutputter; 16 | 17 | public class XmlMultiPatch extends DeferredInitFilterReader { 18 | XmlPatchSpec spec; 19 | String path; 20 | 21 | public XmlMultiPatch(Reader in) { 22 | super(in); 23 | } 24 | 25 | @Override 26 | protected void initialize() { 27 | if (spec == null) { 28 | throw new RuntimeException("missing 'spec' parameter, value must be XmlPatchSpec instance"); 29 | } 30 | 31 | if (path == null) { 32 | throw new RuntimeException("missing 'path' parameter, relative path to file being patched"); 33 | } 34 | 35 | try { 36 | in = buildFilterChain(in, path, spec.resolve()); 37 | } catch (IOException e) { 38 | throw new RuntimeException(e); 39 | } 40 | } 41 | 42 | protected Reader buildFilterChain(Reader source, String sourcePath, AssembledPatch patch) throws IOException { 43 | for (Element diff : patch.getDiffs(sourcePath)) { 44 | diff = (Element) diff.clone(); 45 | diff.removeAttribute("file"); 46 | 47 | final File tempDiff = saveToTempFile(diff); 48 | 49 | source = new XmlPatch(source) { 50 | { 51 | setPatch(tempDiff.getAbsolutePath()); 52 | } 53 | 54 | @Override 55 | public void close() throws IOException { 56 | super.close(); 57 | FileUtils.deleteQuietly(tempDiff); 58 | } 59 | }; 60 | } 61 | 62 | return source; 63 | } 64 | 65 | private File saveToTempFile(Element diff) throws IOException { 66 | Format format = Format.getRawFormat(); 67 | format.setOmitDeclaration(true); 68 | XMLOutputter outputter = new XMLOutputter(format); 69 | 70 | File temp = File.createTempFile("xml-patch-diff-", ".xml"); 71 | try (FileOutputStream os = new FileOutputStream(temp)) { 72 | outputter.output(diff, os); 73 | } 74 | 75 | Log.debug("created temp file: " + temp.getAbsolutePath()); 76 | 77 | return temp; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/dnault/xmlpatch/filter/multi/XmlPatchSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.github.dnault.xmlpatch.filter.multi 2 | 3 | import com.github.dnault.xmlpatch.batch.AssembledPatch 4 | import org.gradle.api.file.FileCollection 5 | import org.gradle.api.internal.file.collections.FileCollectionAdapter 6 | import org.gradle.api.internal.file.collections.MinimalFileSet 7 | 8 | 9 | class XmlPatchSpec { 10 | File patch; 11 | XmlPatchOptions options; 12 | 13 | AssembledPatch assembled; 14 | 15 | XmlPatchSpec(Map options = [:], File patch) { 16 | this.patch = patch 17 | this.options = new XmlPatchOptions(options) 18 | } 19 | 20 | XmlPatchSpec(Map options = [:], String patch) { 21 | this(options, new File(patch)) 22 | } 23 | 24 | AssembledPatch resolve() { 25 | if (assembled == null) { 26 | assembled = new AssembledPatch(patch) 27 | } 28 | return assembled 29 | } 30 | 31 | FileCollection getPatchFragments() { 32 | return new FileCollectionAdapter(new MinimalFileSet() { 33 | @Override 34 | public Set getFiles() { 35 | return resolve().getPatchFiles(); 36 | } 37 | 38 | @Override 39 | public String getDisplayName() { 40 | return "Patch fragments referenced by ${patch.absolutePath}" 41 | } 42 | }); 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return "PatchSpec{" + 48 | "patch=" + patch + 49 | ", options=" + options + 50 | '}'; 51 | } 52 | 53 | void done() { 54 | Set missingPaths = assembled.sourcePaths.toSet() 55 | missingPaths.removeAll(assembled.accessedPaths) 56 | if (!missingPaths.isEmpty()) { 57 | def message = "Some files targeted by XML patch ${patch.absolutePath} were not filtered. " + 58 | "Missing target paths: ${missingPaths}" 59 | throw new FileNotFoundException(message) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/github/dnault/xmlpatch/BatchPatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 David Nault and contributors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.dnault.xmlpatch; 18 | 19 | import static com.github.dnault.xmlpatch.internal.XmlHelper.getChildren; 20 | import static java.util.Arrays.asList; 21 | 22 | import java.io.ByteArrayInputStream; 23 | import java.io.File; 24 | import java.io.FileInputStream; 25 | import java.io.FileNotFoundException; 26 | import java.io.FileOutputStream; 27 | import java.io.IOException; 28 | import java.io.InputStream; 29 | import java.io.OutputStream; 30 | import java.util.ArrayList; 31 | import java.util.HashSet; 32 | import java.util.List; 33 | import java.util.Set; 34 | 35 | import com.github.dnault.xmlpatch.batch.PatchAssembler; 36 | import com.github.dnault.xmlpatch.internal.IoHelper; 37 | import com.github.dnault.xmlpatch.internal.Log; 38 | import com.github.dnault.xmlpatch.internal.XmlHelper; 39 | import joptsimple.OptionException; 40 | import joptsimple.OptionParser; 41 | import joptsimple.OptionSet; 42 | import joptsimple.OptionSpec; 43 | import org.jdom2.Document; 44 | import org.jdom2.Element; 45 | import org.jdom2.output.Format; 46 | import org.jdom2.output.XMLOutputter; 47 | 48 | public class BatchPatcher { 49 | 50 | private static OptionSet parseOptions(OptionParser parser, String... args) throws IOException { 51 | try { 52 | return parser.parse(args); 53 | } catch (OptionException e) { 54 | System.err.println(e.getMessage()); 55 | System.err.println(); 56 | parser.printHelpOn(System.err); 57 | System.exit(1); 58 | throw new Error(); 59 | } 60 | } 61 | 62 | public static void main(String... args) throws Exception { 63 | patch(args); 64 | 65 | 66 | //patch("--patch", "/tmp/patch.xml", "--srcdir", "/tmp/srcdir", "--destdir", "/tmp/patchdest"); 67 | } 68 | 69 | public static void patch(String... args) throws Exception { 70 | OptionParser parser = new OptionParser(); 71 | 72 | OptionSpec helpOption = parser.acceptsAll(asList("help", "?"), "display this help message"); 73 | 74 | OptionSpec patchOption = parser.accepts("patch", "patch file to apply").withRequiredArg().ofType(File.class).required(); 75 | 76 | OptionSpec srcdirOption = parser.accepts("srcdir", "for multi-file patches, specifies the base dir for the files to be patched") 77 | .withRequiredArg().ofType(File.class).required(); 78 | 79 | OptionSpec destdirOption = parser.accepts("destdir", "for multi-file patches, specifies the output directory. (default: apply the patch in-place)") 80 | .withRequiredArg().ofType(File.class); 81 | 82 | OptionSet options = parseOptions(parser, args); 83 | 84 | if (options.has(helpOption)) { 85 | parser.printHelpOn(System.out); 86 | System.exit(0); 87 | } 88 | 89 | File patchFile = patchOption.value(options); 90 | assertFileExists(patchFile, "patch"); 91 | 92 | File srcdir = srcdirOption.value(options); 93 | assertDirectoryExists(srcdir, "srcdir"); 94 | 95 | File destdir = options.has(destdirOption) ? destdirOption.value(options) : srcdir; 96 | if (destdir.exists() && !destdir.isDirectory()) { 97 | throw new IllegalArgumentException("destdir is not a directory: " + destdir.getAbsolutePath()); 98 | } 99 | 100 | File tempdir = IoHelper.createTempDir(); 101 | 102 | try { 103 | Set relativePathsOfPatchedFiles = patch( 104 | assemblePatchDocument(patchFile), srcdir, destdir, tempdir); 105 | 106 | Log.info("writing patched files to destdir: " + destdir.getAbsolutePath()); 107 | for (String path : relativePathsOfPatchedFiles) { 108 | File tempFile = new File(tempdir, path); 109 | File destFile = new File(destdir, path); 110 | IoHelper.makeParentDirectory(destFile); 111 | IoHelper.move(tempFile, destFile); 112 | } 113 | 114 | IoHelper.deleteDirectory(tempdir); 115 | 116 | } catch (PatchException e) { 117 | Log.error(e.getMessage()); 118 | System.exit(1); 119 | } 120 | } 121 | 122 | public static void assertFileExists(File f, String argname) throws FileNotFoundException { 123 | if (!f.exists()) { 124 | throw new FileNotFoundException(argname + " file not found: " + f.getAbsolutePath()); 125 | } 126 | if (!f.isFile()) { 127 | throw new IllegalArgumentException(argname + " is not a file: " + f.getAbsolutePath()); 128 | } 129 | } 130 | 131 | public static void assertDirectoryExists(File f, String argname) throws FileNotFoundException { 132 | if (!f.exists()) { 133 | throw new FileNotFoundException(argname + " directory not found: " + f.getAbsolutePath()); 134 | } 135 | if (!f.isDirectory()) { 136 | throw new IllegalArgumentException(argname + " is not a directory: " + f.getAbsolutePath()); 137 | } 138 | } 139 | 140 | private static List assemblePatchDocument(File patchFile) throws Exception { 141 | return new PatchAssembler().assemble(patchFile).getDiffs(); 142 | } 143 | 144 | private static Set patch(List diffs, File srcdir, File destdir, File tempdir) throws Exception { 145 | 146 | Set outputFilePaths = new HashSet<>(); 147 | 148 | for (Element diff : diffs) { 149 | 150 | if (diff.getAttribute("file") == null) { 151 | throw new IllegalArgumentException("diff element missing 'file' attribute"); 152 | } 153 | 154 | String srcfilePath = diff.getAttributeValue("file"); 155 | 156 | File fileToPatch = new File(srcfilePath); 157 | if (fileToPatch.isAbsolute()) { 158 | throw new IllegalArgumentException("not a relative path: " + srcfilePath); 159 | } 160 | 161 | outputFilePaths.add(srcfilePath); 162 | 163 | File alreadyInTempDir = new File(tempdir, srcfilePath); 164 | fileToPatch = alreadyInTempDir.exists() ? alreadyInTempDir : new File(srcdir, srcfilePath); 165 | 166 | Log.info("patching " + srcfilePath + " [from " + fileToPatch.getAbsolutePath() + "]"); 167 | 168 | diff.removeAttribute("file"); 169 | Format format = Format.getRawFormat(); 170 | format.setOmitDeclaration(true); 171 | XMLOutputter outputter = new XMLOutputter(format); 172 | String s = outputter.outputString(diff); 173 | 174 | InputStream diffStream = new ByteArrayInputStream(s.getBytes("UTF-8")); 175 | InputStream inputStream = new FileInputStream(fileToPatch); 176 | 177 | File outputFile = File.createTempFile("xmlpatch", ".xml"); 178 | OutputStream outputStream = new FileOutputStream(outputFile); 179 | 180 | Patcher.patch(inputStream, diffStream, outputStream); 181 | 182 | inputStream.close(); 183 | outputStream.close(); 184 | 185 | File outputTemp = new File(tempdir, srcfilePath); 186 | File tempParentDir = outputTemp.getParentFile(); 187 | 188 | if (!tempParentDir.exists() && !tempParentDir.mkdirs()) { 189 | throw new IOException("failed to create directory: " + tempParentDir.getAbsolutePath()); 190 | } 191 | 192 | IoHelper.move(outputFile, outputTemp); 193 | } 194 | return outputFilePaths; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/main/java/com/github/dnault/xmlpatch/CommandLineDriver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 David Nault and contributors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.dnault.xmlpatch; 18 | 19 | import java.io.InputStream; 20 | import java.io.FileInputStream; 21 | import java.io.OutputStream; 22 | import java.io.FileOutputStream; 23 | import java.io.FileNotFoundException; 24 | import java.io.IOException; 25 | import java.io.File; 26 | 27 | public class CommandLineDriver { 28 | 29 | public static void main(String... args) throws Exception { 30 | 31 | if (args.length == 0) { 32 | usage(); 33 | System.exit(0); 34 | } 35 | 36 | if (args.length != 3) { 37 | System.err.println("incorrect number of arguments"); 38 | usage(); 39 | System.exit(1); 40 | } 41 | 42 | String input = args[0]; 43 | String patch = args[1]; 44 | String output = args[2]; 45 | 46 | if (input.equals("-") && patch.equals("-")) { 47 | System.err.println("input and patch may not both come from standard input"); 48 | System.exit(1); 49 | } 50 | 51 | boolean patchInPlace = !input.equals("-") && isSameFile(input, output); 52 | 53 | if (patchInPlace) { 54 | output = File.createTempFile("xmlpatch", ".xml").getAbsolutePath(); 55 | } 56 | 57 | try { 58 | InputStream inputStream = input.equals("-") ? System.in : new FileInputStream(input); 59 | InputStream patchStream = patch.equals("-") ? System.in : new FileInputStream(patch); 60 | OutputStream outputStream = output.equals("-") ? System.out : new FileOutputStream(output); 61 | 62 | Patcher.patch(inputStream, patchStream, outputStream); 63 | 64 | if (patchInPlace) { 65 | if (!new File(output).renameTo(new File(input))) { 66 | throw new IOException("could not rename temp file to " + input); 67 | } 68 | } 69 | } 70 | catch (FileNotFoundException e) { 71 | System.err.println("ERROR: Could not access file: " + e.getMessage()); 72 | System.exit(1); 73 | } 74 | } 75 | 76 | private static boolean isSameFile(String pathA, String pathB) throws IOException { 77 | return new File(pathA).getCanonicalPath().equals(new File(pathB).getCanonicalPath()); 78 | } 79 | 80 | private static void usage() { 81 | System.err.println("USAGE: java -jar xml-patch.jar "); 82 | System.err.println(" A dash (-) may be used to indicate standard input / output"); 83 | System.err.println(" The patch is an XML diff document as defined by RFC 5261"); 84 | System.err.println(" or an XML patch document as defined by RFC 7351"); 85 | System.exit(1); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/github/dnault/xmlpatch/ErrorCondition.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 David Nault and contributors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.dnault.xmlpatch; 18 | 19 | public enum ErrorCondition { 20 | INVALID_ATTRIBUTE_VALUE, 21 | INVALID_CHARACTER_SET, 22 | INVALID_DIFF_FORMAT, 23 | INVALID_ENTITY_DECLARATION, 24 | INVALID_NAMESPACE_PREFIX, 25 | INVALID_NAMESPACE_URI, 26 | INVALID_NODE_TYPES, 27 | INVALID_PATCH_DIRECTIVE, 28 | INVALID_ROOT_ELEMENT_OPERATION, 29 | INVALID_XML_PROLOG_OPERATION, 30 | INVALID_WHITESPACE_DIRECTIVE, 31 | UNLOCATED_NODE, 32 | UNSUPORTED_ID_FUNCTION, 33 | UNSUPPORTED_XML_ID, 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/github/dnault/xmlpatch/PatchException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 David Nault and contributors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.dnault.xmlpatch; 18 | 19 | 20 | public class PatchException extends RuntimeException { 21 | 22 | private final ErrorCondition errorCondition; 23 | 24 | public PatchException(ErrorCondition errorCondition) { 25 | this.errorCondition = errorCondition; 26 | } 27 | 28 | public PatchException(ErrorCondition errorCondition, String message, 29 | Throwable cause) { 30 | super(message, cause); 31 | this.errorCondition = errorCondition; 32 | } 33 | 34 | public PatchException(ErrorCondition errorCondition, String message) { 35 | super(message); 36 | this.errorCondition = errorCondition; 37 | } 38 | 39 | public PatchException(ErrorCondition errorCondition, Throwable t) { 40 | super(t); 41 | this.errorCondition = errorCondition; 42 | } 43 | 44 | public ErrorCondition getErrorCondition() { 45 | return errorCondition; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/github/dnault/xmlpatch/Patcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 David Nault and contributors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.dnault.xmlpatch; 18 | 19 | import com.github.dnault.xmlpatch.internal.XmlHelper; 20 | import org.jdom2.Attribute; 21 | import org.jdom2.Namespace; 22 | import org.jdom2.Text; 23 | import org.jdom2.Comment; 24 | import org.jdom2.ProcessingInstruction; 25 | import org.jdom2.Content; 26 | import org.jdom2.Element; 27 | import org.jdom2.Document; 28 | import org.jdom2.Parent; 29 | import org.jdom2.IllegalAddException; 30 | import org.jdom2.IllegalNameException; 31 | import org.jdom2.JDOMException; 32 | import org.jdom2.output.Format; 33 | import org.jdom2.output.XMLOutputter; 34 | import org.jdom2.xpath.XPath; 35 | 36 | import java.io.ByteArrayInputStream; 37 | import java.io.IOException; 38 | import java.io.InputStream; 39 | import java.io.OutputStream; 40 | 41 | import java.util.List; 42 | import java.util.Map; 43 | import java.util.Collections; 44 | import java.util.ArrayList; 45 | import java.util.Arrays; 46 | import java.util.Iterator; 47 | 48 | import static com.github.dnault.xmlpatch.internal.XmlHelper.getInScopeNamespaceDeclarations; 49 | 50 | @SuppressWarnings("unchecked") 51 | public class Patcher { 52 | 53 | static private boolean lenient = true; 54 | static private boolean trimMultilineText = true; 55 | 56 | public static void patch(InputStream target, InputStream diff, 57 | OutputStream result) throws IOException { 58 | 59 | try { 60 | Document targetDoc = XmlHelper.parse(target); 61 | Document diffDoc = XmlHelper.parse(diff); 62 | 63 | Element diffRoot = diffDoc.getRootElement(); 64 | for (Object o : diffRoot.getChildren()) { 65 | patch(targetDoc, (Element) o); 66 | } 67 | 68 | XMLOutputter outputter = new XMLOutputter(); 69 | 70 | // Use the separator that is appropriate for the platform. 71 | Format format = Format.getRawFormat(); 72 | format.setLineSeparator(System.getProperty("line.separator")); 73 | outputter.setFormat(format); 74 | 75 | outputter.output(targetDoc, result); 76 | } catch (JDOMException e) { 77 | throw new PatchException(ErrorCondition.INVALID_DIFF_FORMAT, e); 78 | } 79 | } 80 | 81 | private static void patch(Document target, Element patch) throws JDOMException { 82 | String operation = patch.getName(); 83 | switch (operation) { 84 | case "add": 85 | add(target, patch); 86 | break; 87 | case "replace": 88 | replace(target, patch); 89 | break; 90 | case "remove": 91 | remove(target, patch); 92 | break; 93 | default: 94 | throw new RuntimeException("unknown operation: " + operation); 95 | } 96 | } 97 | 98 | private static void throwIfNotSingleNodeOfType(List content, Class expectedClass) { 99 | if (content.size() != 1 || !content.get(0).getClass().equals(expectedClass)) { 100 | throw new PatchException(ErrorCondition.INVALID_NODE_TYPES, "expected replacement to be a single content node of type " 101 | + expectedClass.getSimpleName()); 102 | } 103 | } 104 | 105 | private static void replace(Document target, Element patch) throws JDOMException { 106 | for (Object node : selectNodes(target, patch)) { 107 | doReplace(patch, node); 108 | } 109 | } 110 | 111 | public static void main(String[] args) throws Exception { 112 | Document doc = XmlHelper.parse(new ByteArrayInputStream("".getBytes("UTF-8"))); 113 | Element patch = doc.getRootElement(); 114 | 115 | Document d = new Document(); 116 | Element e = new Element("foo").setAttribute("message", "goodbye").setText("asdasda"); 117 | d.addContent(e); 118 | 119 | doReplace(patch, e.getContent(0)); 120 | System.out.println("[" + e.getText() + "]"); 121 | } 122 | 123 | private static String getTextMaybeTrim(Element patch) { 124 | // Implement a non-standard "trim" attribute on patch nodes. 125 | String value = patch.getText(); 126 | String override = patch.getAttributeValue("trim"); 127 | if (override != null) { 128 | if (override.equals("true")) { 129 | return value.trim(); 130 | } 131 | if (override.equals("false")) { 132 | return value; 133 | } 134 | throw new RuntimeException("expected 'trim' attribute to be 'true' or 'false' but found " + override); 135 | } 136 | 137 | return trimMultilineText && isMultiline(value) ? value.trim() : value; 138 | } 139 | 140 | private static void doReplace(Element patch, Object node) throws JDOMException { 141 | 142 | if (node instanceof Attribute) { 143 | for (Object o : patch.getContent()) { 144 | if (!(o instanceof Text)) { 145 | throw new PatchException(ErrorCondition.INVALID_NODE_TYPES, "attribute value replacement must be text"); 146 | } 147 | } 148 | 149 | ((Attribute) node).setValue(getTextMaybeTrim(patch)); 150 | return; 151 | } 152 | 153 | if (node instanceof Element || node instanceof Comment || node instanceof ProcessingInstruction) { 154 | List replacement = patch.cloneContent(); 155 | 156 | if (lenient) { 157 | // ignore whitespace siblings so the diff document can be pretty 158 | for (Iterator i = replacement.iterator(); i.hasNext(); ) { 159 | if (isWhitespace(i.next())) { 160 | i.remove(); 161 | } 162 | } 163 | } 164 | 165 | throwIfNotSingleNodeOfType(replacement, node.getClass()); 166 | 167 | Content replaceMe = (Content) node; 168 | Parent p = replaceMe.getParent(); 169 | int index = XmlHelper.indexOf(p, replaceMe); 170 | replaceMe.detach(); 171 | 172 | if (p instanceof Element) { 173 | canonicalizeNamespaces((Element) p, replacement); 174 | ((Element) p).addContent(index, replacement); 175 | 176 | } else if (p instanceof Document) { 177 | if (!(node instanceof Element)) { 178 | throw new PatchException(ErrorCondition.INVALID_XML_PROLOG_OPERATION, 179 | "can't replace prolog nodes"); 180 | } 181 | 182 | ((Document) p).setRootElement((Element) replacement.get(0)); 183 | 184 | } else { 185 | // shouldn't happen. 186 | throw new RuntimeException("expected Parent to be either Document or Element but found " + p.getClass()); 187 | } 188 | 189 | return; 190 | } 191 | 192 | if (node instanceof Text) { 193 | List replacement = patch.cloneContent(); 194 | if (!replacement.isEmpty()) { 195 | throwIfNotSingleNodeOfType(replacement, Text.class); 196 | } 197 | 198 | Content replaceMe = (Content) node; 199 | Element p = replaceMe.getParentElement(); 200 | int index = XmlHelper.indexOf(p, replaceMe); 201 | replaceMe.detach(); 202 | 203 | String replacementText = getTextMaybeTrim(patch); 204 | if (replacementText.length() > 0) { 205 | p.addContent(index, new Text(replacementText)); 206 | } 207 | 208 | return; 209 | } 210 | 211 | 212 | if (node instanceof Namespace) { 213 | if (true) { 214 | throw new UnsupportedOperationException("removing namespace declarations is not yet implemented"); 215 | } 216 | 217 | 218 | // TODO: This needs to be the element in the target that 219 | // contains the namespace declaration 220 | Element parent = new Element(""); 221 | 222 | Namespace ns = (Namespace) node; 223 | 224 | String newUri = getTextMaybeTrim(patch); 225 | Namespace newNamespace = createNamespace(ns.getPrefix(), newUri); 226 | 227 | if (parent.getAdditionalNamespaces().contains(ns)) { 228 | parent.removeNamespaceDeclaration(ns); 229 | parent.addNamespaceDeclaration(newNamespace); 230 | } else if (parent.getNamespace().getPrefix().equals(ns.getPrefix())) { 231 | parent.setNamespace(newNamespace); 232 | } else { 233 | throw new PatchException(ErrorCondition.UNLOCATED_NODE); 234 | } 235 | return; 236 | } 237 | 238 | throw new PatchException(ErrorCondition.INVALID_PATCH_DIRECTIVE, 239 | "target node '" + node + "' was not an Attribute, Element, Comment, Text, Processing Instruction, or Namespace"); 240 | } 241 | 242 | private static boolean isMultiline(String replacement) { 243 | return replacement.contains("\n") || replacement.contains("\r"); 244 | } 245 | 246 | private static boolean isWhitespace(Content node) { 247 | // todo stricter interpretation of whitespace? 248 | return node instanceof Text && node.getValue().trim().length() == 0; 249 | } 250 | 251 | private static void add(Document target, Element patch) throws JDOMException { 252 | for (Object node : selectNodes(target, patch)) { 253 | doAdd(patch, node); 254 | } 255 | } 256 | 257 | private static void doAdd(Element patch, Object nodeObject) throws JDOMException { 258 | String position = patch.getAttributeValue("pos"); 259 | String type = patch.getAttributeValue("type"); 260 | 261 | Content node = (Content) nodeObject; 262 | 263 | if (type != null) { 264 | if (type.startsWith("@")) { 265 | addAttribute(patch, type.substring(1), asElement(node)); 266 | return; 267 | } 268 | if (type.startsWith("namespace::")) { 269 | addNamespaceDeclaration(patch, type.substring(11), asElement(node)); 270 | return; 271 | } 272 | // todo validation 273 | } 274 | 275 | if ("before".equals(position) || "after".equals(position)) { 276 | if (node.getParentElement() == null) { 277 | for (Object o : patch.getContent()) { 278 | if (!(o instanceof Comment) && !(o instanceof ProcessingInstruction)) { 279 | throw new PatchException(ErrorCondition.INVALID_ROOT_ELEMENT_OPERATION); 280 | } 281 | } 282 | } 283 | } 284 | 285 | List newContent = patch.cloneContent(); 286 | try { 287 | if (position == null) { 288 | // default is "append" 289 | Element e = asElement(node); 290 | canonicalizeNamespaces(e, newContent); 291 | e.getContent().addAll(newContent); 292 | } else if ("prepend".equals(position)) { 293 | Element e = asElement(node); 294 | canonicalizeNamespaces(e, newContent); 295 | e.getContent().addAll(0, newContent); 296 | } else if ("before".equals(position)) { 297 | Parent p = node.getParent(); 298 | 299 | if (p instanceof Element) { 300 | canonicalizeNamespaces((Element) p, newContent); 301 | } 302 | 303 | int nodeIndex = XmlHelper.indexOf(p, node); 304 | p.getContent().addAll(nodeIndex, newContent); 305 | } else if ("after".equals(position)) { 306 | Parent p = node.getParent(); 307 | 308 | if (p instanceof Element) { 309 | canonicalizeNamespaces((Element) p, newContent); 310 | } 311 | 312 | int nodeIndex = XmlHelper.indexOf(p, node); 313 | p.getContent().addAll(nodeIndex + 1, newContent); 314 | } else { 315 | throw new PatchException(ErrorCondition.INVALID_DIFF_FORMAT, 316 | "unrecognized position '" + position + "' for add; expected one of " + Arrays.toString(new String[]{"before", "after", "prepend"})); 317 | } 318 | } catch (IllegalAddException e) { 319 | // todo nice message 320 | throw new PatchException(ErrorCondition.INVALID_PATCH_DIRECTIVE, e); 321 | } 322 | 323 | } 324 | 325 | private static void addNamespaceDeclaration(Element patch, String prefix, Element target) { 326 | 327 | Namespace ns = createNamespace(prefix, getTextMaybeTrim(patch)); 328 | target.addNamespaceDeclaration(ns); 329 | } 330 | 331 | private static Namespace createNamespace(String prefix, String uri) { 332 | try { 333 | return Namespace.getNamespace(prefix, uri); 334 | } catch (IllegalNameException e) { 335 | throw new PatchException(ErrorCondition.INVALID_NAMESPACE_URI, e.getMessage()); 336 | } 337 | } 338 | 339 | private static void addAttribute(Element patch, String name, Element target) { 340 | 341 | String prefix = null; 342 | if (name.contains(":")) { 343 | String[] prefixAndName = name.split(":"); 344 | // todo validate length = 2? 345 | prefix = prefixAndName[0]; 346 | name = prefixAndName[1]; 347 | } 348 | 349 | String value = getTextMaybeTrim(patch); 350 | Attribute a = new Attribute(name, value); 351 | 352 | if (prefix != null) { 353 | Namespace ns = patch.getNamespace(prefix); 354 | if (ns == null) { 355 | throw new PatchException( 356 | ErrorCondition.INVALID_NAMESPACE_PREFIX, 357 | "could not resolve namespace prefix '" + prefix 358 | + "' in the context of the diff document"); 359 | } 360 | a.setNamespace(ns); 361 | } 362 | canonicalizeNamespace(a, getInScopeNamespaceDeclarations(target)); 363 | target.setAttribute(a); 364 | } 365 | 366 | private static void canonicalizeNamespaces(Element scope, 367 | List content) { 368 | 369 | Map prefixToUri = XmlHelper 370 | .getInScopeNamespaceDeclarations(scope); 371 | 372 | for (Content c : content) { 373 | if (c instanceof Element) { 374 | canonicalizeNamespace((Element) c, prefixToUri); 375 | } 376 | } 377 | 378 | } 379 | 380 | private static void canonicalizeNamespace(Attribute a, 381 | Map prefixToUri) { 382 | for (Map.Entry entry : prefixToUri.entrySet()) { 383 | if (a.getNamespaceURI().equals(entry.getValue())) { 384 | if (entry.getKey().equals("")) { 385 | // default namespace doesn't apply to attributes 386 | continue; 387 | } 388 | a.setNamespace(Namespace.getNamespace(entry.getKey(), entry 389 | .getValue())); 390 | break; 391 | 392 | } 393 | } 394 | } 395 | 396 | private static void canonicalizeNamespace(Element e, 397 | Map prefixToUri) { 398 | Namespace ns = e.getNamespace(); 399 | 400 | for (Map.Entry entry : prefixToUri.entrySet()) { 401 | if (ns.getURI().equals(entry.getValue())) { 402 | e.setNamespace(Namespace.getNamespace(entry.getKey(), entry 403 | .getValue())); 404 | break; 405 | } 406 | } 407 | 408 | for (Object o : e.getAttributes()) { 409 | canonicalizeNamespace((Attribute) o, prefixToUri); 410 | } 411 | 412 | for (Object o : e.getChildren()) { 413 | canonicalizeNamespace((Element) o, prefixToUri); 414 | } 415 | } 416 | 417 | private static Element asElement(Content node) { 418 | try { 419 | return (Element) node; 420 | } catch (ClassCastException e) { 421 | throw new PatchException(ErrorCondition.INVALID_PATCH_DIRECTIVE, 422 | "selected node is not an element"); 423 | } 424 | } 425 | 426 | private static List selectNodes(Document target, Element patch) throws JDOMException { 427 | 428 | boolean isMultiSelect = false; 429 | 430 | String selector = patch.getAttributeValue("sel"); 431 | if (selector == null) { 432 | selector = patch.getAttributeValue("msel"); 433 | isMultiSelect = true; 434 | } 435 | 436 | XPath xpath = XPath.newInstance(selector); 437 | bindNamespacePrefixes(xpath, patch); 438 | List content = xpath.selectNodes(target); 439 | 440 | if (content.isEmpty()) { 441 | throw new PatchException(ErrorCondition.UNLOCATED_NODE, 442 | "no matches for selector \"" + selector + "\""); 443 | } 444 | if (!isMultiSelect && content.size() > 1) { 445 | throw new PatchException(ErrorCondition.UNLOCATED_NODE, 446 | "more that one match for selector \"" + selector + "\" -- if you want to select multiple nodes, use the 'msel' attribute instead of 'sel'."); 447 | } 448 | return content; 449 | } 450 | 451 | private static void bindNamespacePrefixes(XPath xpath, Element patch) { 452 | List chain = new ArrayList<>(); 453 | for (Element e = patch; e != null; e = e.getParentElement()) { 454 | chain.add(e); 455 | } 456 | 457 | // namespace definitions on the child should override any parent defintions with the same prefix 458 | Collections.reverse(chain); 459 | 460 | for (Element e : chain) { 461 | for (Object o : e.getAdditionalNamespaces()) { 462 | xpath.addNamespace((Namespace) o); 463 | } 464 | } 465 | } 466 | 467 | private static void remove(Document target, Element patch) throws JDOMException { 468 | for (Object node : selectNodes(target, patch)) { 469 | doRemove(patch, node); 470 | } 471 | } 472 | 473 | private static void doRemove(Element patch, Object node) throws JDOMException { 474 | 475 | if (node instanceof Element || node instanceof Comment || node instanceof ProcessingInstruction) { 476 | 477 | String ws = patch.getAttributeValue("ws"); 478 | boolean before = "both".equals(ws) || "before".equals(ws); 479 | boolean after = "both".equals(ws) || "after".equals(ws); 480 | 481 | Content c = (Content) node; 482 | Element e = c.getParentElement(); 483 | if (e == null) { 484 | throw new PatchException(ErrorCondition.INVALID_ROOT_ELEMENT_OPERATION, 485 | "can't remove root element"); 486 | } 487 | 488 | int index = e.indexOf(c); 489 | List nodesToDetach = new ArrayList<>(); 490 | nodesToDetach.add(c); 491 | 492 | if (before) { 493 | nodesToDetach.add(getWhitespace(e, index - 1)); 494 | } 495 | if (after) { 496 | nodesToDetach.add(getWhitespace(e, index + 1)); 497 | } 498 | 499 | for (Content detachMe : nodesToDetach) { 500 | detachMe.detach(); 501 | } 502 | 503 | return; 504 | } 505 | 506 | if (patch.getAttribute("ws") != null) { 507 | throw new PatchException(ErrorCondition.INVALID_PATCH_DIRECTIVE, 508 | "The 'ws' attribute is not allowed when removing " + 509 | "Attribute, Text or Namespace nodes."); 510 | } 511 | 512 | if (node instanceof Attribute) { 513 | Attribute a = (Attribute) node; 514 | a.getParent().removeAttribute(a); 515 | return; 516 | } 517 | 518 | if (node instanceof Text) { 519 | ((Content) node).detach(); 520 | return; 521 | } 522 | 523 | if (node instanceof Namespace) { 524 | throw new UnsupportedOperationException("removing namespace declarations is not yet implemented"); 525 | // return; 526 | } 527 | } 528 | 529 | private static Text getWhitespace(Element parent, int i) { 530 | 531 | try { 532 | Content c = parent.getContent(i); 533 | if (isWhitespace(c)) { 534 | return (Text) c; 535 | } 536 | } catch (IndexOutOfBoundsException noSuchSibling) { 537 | // invalid whitepace directive 538 | } 539 | 540 | throw new PatchException(ErrorCondition.INVALID_WHITESPACE_DIRECTIVE, 541 | "sibling is not a whitespace node"); 542 | } 543 | 544 | } 545 | -------------------------------------------------------------------------------- /src/main/java/com/github/dnault/xmlpatch/ant/AbstractPatchTask.java: -------------------------------------------------------------------------------- 1 | package com.github.dnault.xmlpatch.ant; 2 | 3 | import java.io.File; 4 | import java.io.FileOutputStream; 5 | import java.io.OutputStream; 6 | import java.nio.charset.StandardCharsets; 7 | 8 | import org.apache.tools.ant.BuildException; 9 | import org.apache.tools.ant.Task; 10 | 11 | public abstract class AbstractPatchTask extends Task { 12 | private String src; 13 | private String dest; 14 | private String patch; 15 | private String inlinePatch; 16 | 17 | public String getSrc() { 18 | return src; 19 | } 20 | 21 | public void setSrc(String src) { 22 | this.src = new File(src).getAbsolutePath(); 23 | } 24 | 25 | public String getDest() { 26 | return dest; 27 | } 28 | 29 | public void setDest(String dest) { 30 | this.dest = new File(dest).getAbsolutePath(); 31 | } 32 | 33 | public String getPatch() { 34 | return patch; 35 | } 36 | 37 | public void setPatch(String patch) { 38 | this.patch = patch; 39 | } 40 | 41 | public String getInlinePatch() { 42 | return inlinePatch; 43 | } 44 | 45 | public void addText(String text) { 46 | this.inlinePatch = getProject().replaceProperties(text); 47 | } 48 | 49 | protected void checkArgs() { 50 | if (src == null) { 51 | throw new BuildException("missing 'src' attribute"); 52 | } 53 | 54 | if (patch == null && inlinePatch == null) { 55 | throw new BuildException("missing 'patch' attribute or nested text"); 56 | } 57 | 58 | if (patch != null && inlinePatch != null) { 59 | throw new BuildException("must not provide both 'patch' attribute and nested text"); 60 | } 61 | 62 | if (dest == null) { 63 | dest = src; 64 | } 65 | } 66 | 67 | public void execute() { 68 | checkArgs(); 69 | 70 | File tempFile = null; 71 | try { 72 | if (getInlinePatch() != null) { 73 | try { 74 | tempFile = File.createTempFile("inline-patch-", ".xml"); 75 | setPatch(tempFile.getAbsolutePath()); 76 | 77 | try (OutputStream os = new FileOutputStream(tempFile)) { 78 | os.write(getInlinePatch().getBytes(StandardCharsets.UTF_8)); 79 | } 80 | } catch (Exception e) { 81 | throw new BuildException(e); 82 | } 83 | } 84 | 85 | try { 86 | log("Applying XML patch"); 87 | log(" src: " + getSrc()); 88 | log(" patch: " + getPatch()); 89 | log(" dest: " + getDest()); 90 | 91 | doPatch(); 92 | 93 | } catch (Exception e) { 94 | throw new BuildException(e); 95 | } 96 | } finally { 97 | if (tempFile != null) { 98 | if (!tempFile.delete()) { 99 | log("Failed to delete temp file for inline XML patch: " + tempFile.getAbsolutePath()); 100 | } 101 | } 102 | } 103 | } 104 | 105 | protected abstract void doPatch() throws Exception; 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/com/github/dnault/xmlpatch/ant/AntXmlPatchFilter.java: -------------------------------------------------------------------------------- 1 | package com.github.dnault.xmlpatch.ant; 2 | 3 | import java.io.Reader; 4 | import java.io.StringReader; 5 | 6 | import com.github.dnault.xmlpatch.filter.XmlPatch; 7 | import org.apache.tools.ant.filters.BaseFilterReader; 8 | import org.apache.tools.ant.filters.ChainableReader; 9 | import org.apache.tools.ant.types.Parameter; 10 | import org.apache.tools.ant.types.Parameterizable; 11 | 12 | public class AntXmlPatchFilter extends XmlPatch implements ChainableReader, Parameterizable { 13 | /** 14 | * Constructor for "dummy" instances. 15 | * 16 | * @see BaseFilterReader#BaseFilterReader() 17 | */ 18 | @SuppressWarnings("unused") 19 | public AntXmlPatchFilter() { 20 | super(new StringReader("")); 21 | org.apache.tools.ant.util.FileUtils.close(this); 22 | } 23 | 24 | public AntXmlPatchFilter(Reader in) { 25 | super(in); 26 | } 27 | 28 | @Override 29 | public Reader chain(Reader reader) { 30 | AntXmlPatchFilter r = new AntXmlPatchFilter(reader); 31 | r.setPatch(getPatch()); 32 | return r; 33 | } 34 | 35 | @Override 36 | public void setParameters(Parameter[] parameters) { 37 | if (parameters != null) { 38 | for (Parameter p : parameters) { 39 | if (p.getName().equalsIgnoreCase("patch")) { 40 | setPatch(p.getValue()); 41 | } else { 42 | throw new RuntimeException("unrecognized parameter: " + p.getName()); 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/github/dnault/xmlpatch/ant/PatchXmlDir.java: -------------------------------------------------------------------------------- 1 | package com.github.dnault.xmlpatch.ant; 2 | 3 | import com.github.dnault.xmlpatch.BatchPatcher; 4 | 5 | public class PatchXmlDir extends AbstractPatchTask { 6 | @Override 7 | protected void doPatch() throws Exception { 8 | BatchPatcher.patch("--patch=" + getPatch(), "--srcdir=" + getSrc(), "--destdir=" + getDest()); 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/main/java/com/github/dnault/xmlpatch/ant/PatchXmlFile.java: -------------------------------------------------------------------------------- 1 | package com.github.dnault.xmlpatch.ant; 2 | 3 | import com.github.dnault.xmlpatch.CommandLineDriver; 4 | 5 | public class PatchXmlFile extends AbstractPatchTask { 6 | @Override 7 | protected void doPatch() throws Exception { 8 | CommandLineDriver.main(getSrc(), getPatch(), getDest()); 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/main/java/com/github/dnault/xmlpatch/batch/AssembledPatch.java: -------------------------------------------------------------------------------- 1 | package com.github.dnault.xmlpatch.batch; 2 | 3 | import static java.util.Objects.requireNonNull; 4 | 5 | import java.io.File; 6 | import java.util.ArrayList; 7 | import java.util.HashSet; 8 | import java.util.LinkedHashSet; 9 | import java.util.List; 10 | import java.util.Set; 11 | 12 | import org.jdom2.Element; 13 | 14 | public class AssembledPatch { 15 | private LinkedHashSet patchFiles = new LinkedHashSet<>(); 16 | private List diffs = new ArrayList<>(); 17 | private Set accessedPaths = new HashSet<>(); 18 | 19 | public AssembledPatch() { 20 | } 21 | 22 | public AssembledPatch(File patch) throws Exception { 23 | new PatchAssembler().assembleRecursive(patch, this); 24 | } 25 | 26 | /** 27 | * @return the set of files that comprise the patch 28 | */ 29 | public LinkedHashSet getPatchFiles() { 30 | return patchFiles; 31 | } 32 | 33 | public boolean addPatchFile(File includedFile) { 34 | return this.patchFiles.add(includedFile); 35 | } 36 | 37 | public List getDiffs() { 38 | return diffs; 39 | } 40 | 41 | public List getDiffs(String sourcePath) { 42 | requireNonNull(sourcePath); 43 | 44 | List matchingDiffs = new ArrayList<>(0); 45 | for (Element e : diffs) { 46 | if (sourcePath.equals(e.getAttributeValue("file"))) { 47 | matchingDiffs.add(e); 48 | accessedPaths.add(sourcePath); 49 | } 50 | } 51 | return matchingDiffs; 52 | } 53 | 54 | public Set getSourcePaths() { 55 | Set sourcePaths = new HashSet<>(); 56 | for (Element e : diffs) { 57 | sourcePaths.add(e.getAttributeValue("file")); 58 | } 59 | return sourcePaths; 60 | } 61 | 62 | public Set getAccessedPaths() { 63 | return accessedPaths; 64 | } 65 | 66 | public void addDif(Element diff) { 67 | this.diffs.add(diff); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/github/dnault/xmlpatch/batch/PatchAssembler.java: -------------------------------------------------------------------------------- 1 | package com.github.dnault.xmlpatch.batch; 2 | 3 | import static com.github.dnault.xmlpatch.internal.XmlHelper.getChildren; 4 | 5 | import java.io.File; 6 | 7 | import com.github.dnault.xmlpatch.internal.Log; 8 | import com.github.dnault.xmlpatch.internal.XmlHelper; 9 | import org.jdom2.Document; 10 | import org.jdom2.Element; 11 | 12 | public class PatchAssembler { 13 | 14 | public AssembledPatch assemble(File patchFile) throws Exception { 15 | AssembledPatch assembled = new AssembledPatch(); 16 | assembleRecursive(patchFile, assembled); 17 | return assembled; 18 | } 19 | 20 | protected void assembleRecursive(File patchFile, AssembledPatch assembled) throws Exception { 21 | 22 | String path = patchFile.getAbsolutePath(); 23 | if (!assembled.addPatchFile(patchFile)) { 24 | // already included this file. 25 | return; 26 | } 27 | 28 | Log.info("Reading XML patch file: " + path); 29 | 30 | Document doc = XmlHelper.parse(patchFile); 31 | Element batchElement = doc.getRootElement(); 32 | 33 | if (!batchElement.getName().equals("diffs")) { 34 | throw new IllegalArgumentException(path + ": expected root element of patch document to be 'diffs' but found '" + batchElement.getName() + "'"); 35 | } 36 | 37 | for (Element diff : getChildren(batchElement)) { 38 | if (diff.getName().equals("include")) { 39 | String includeFilename = diff.getAttributeValue("file"); 40 | if (includeFilename == null) { 41 | throw new IllegalArgumentException(path + ": element 'include' missing 'file' attribute"); 42 | } 43 | 44 | File includeFile = new File(patchFile.getParent(), includeFilename); 45 | assembleRecursive(includeFile, assembled); 46 | continue; 47 | } 48 | 49 | if (!diff.getName().equals("diff")) { 50 | throw new IllegalArgumentException(path + ": unexpected element '" + diff.getName() + "' in patch document, expected 'diff' or 'include'"); 51 | } 52 | 53 | if (diff.getAttribute("file") == null) { 54 | throw new IllegalArgumentException(path + ": 'diff' element missing 'file' attribute"); 55 | } 56 | 57 | assembled.addDif((Element) diff.clone()); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/github/dnault/xmlpatch/filter/XmlPatch.java: -------------------------------------------------------------------------------- 1 | package com.github.dnault.xmlpatch.filter; 2 | 3 | import static java.nio.charset.StandardCharsets.UTF_8; 4 | 5 | import java.io.File; 6 | import java.io.FileOutputStream; 7 | import java.io.FileReader; 8 | import java.io.IOException; 9 | import java.io.OutputStreamWriter; 10 | import java.io.Reader; 11 | import java.io.Writer; 12 | 13 | import com.github.dnault.xmlpatch.CommandLineDriver; 14 | import com.github.dnault.xmlpatch.internal.DeferredInitFilterReader; 15 | import org.apache.commons.io.FileUtils; 16 | import org.apache.commons.io.IOUtils; 17 | 18 | public class XmlPatch extends DeferredInitFilterReader { 19 | private String patch; 20 | private File output; 21 | 22 | public XmlPatch(Reader in) { 23 | super(in); 24 | } 25 | 26 | public void setPatch(String patch) { 27 | if (patch != null) { 28 | patch = new File(patch).getAbsolutePath(); 29 | } 30 | this.patch = patch; 31 | } 32 | 33 | public String getPatch() { 34 | return patch; 35 | } 36 | 37 | @Override 38 | protected void initialize() { 39 | if (patch == null) { 40 | throw new RuntimeException("missing 'patch' parameter, path to patch file"); 41 | } 42 | 43 | try { 44 | File input = File.createTempFile("xml-patch-input-", ".xml"); 45 | try { 46 | output = File.createTempFile("xml-patch-result-", ".xml"); 47 | 48 | try (FileOutputStream os = new FileOutputStream(input); 49 | Writer writer = new OutputStreamWriter(os, UTF_8); 50 | Reader reader = in) { 51 | IOUtils.copy(reader, writer); 52 | } 53 | 54 | CommandLineDriver.main(input.getAbsolutePath(), patch, output.getAbsolutePath()); 55 | in = new FileReader(output); 56 | 57 | } finally { 58 | FileUtils.deleteQuietly(input); 59 | } 60 | 61 | } catch (Exception e) { 62 | throw new RuntimeException(e); 63 | } 64 | } 65 | 66 | @Override 67 | public void close() throws IOException { 68 | try { 69 | super.close(); 70 | } finally { 71 | FileUtils.deleteQuietly(output); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/github/dnault/xmlpatch/filter/multi/XmlPatchOptions.java: -------------------------------------------------------------------------------- 1 | package com.github.dnault.xmlpatch.filter.multi; 2 | 3 | public class XmlPatchOptions { 4 | private boolean failOnMissingPatch = true; 5 | private boolean failOnMissingSourcePath = true; 6 | 7 | public boolean isFailOnMissingPatch() { 8 | return failOnMissingPatch; 9 | } 10 | 11 | public void setFailOnMissingPatch(boolean failOnMissingPatch) { 12 | this.failOnMissingPatch = failOnMissingPatch; 13 | } 14 | 15 | public boolean isFailOnMissingSourcePath() { 16 | return failOnMissingSourcePath; 17 | } 18 | 19 | public void setFailOnMissingSourcePath(boolean failOnMissingSourcePath) { 20 | this.failOnMissingSourcePath = failOnMissingSourcePath; 21 | } 22 | 23 | @Override 24 | public String toString() { 25 | return "XmlPatchOptions{" + 26 | "failOnMissingPatch=" + failOnMissingPatch + 27 | ", failOnMissingSourcePath=" + failOnMissingSourcePath + 28 | '}'; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/dnault/xmlpatch/internal/DeferredInitFilterReader.java: -------------------------------------------------------------------------------- 1 | package com.github.dnault.xmlpatch.internal; 2 | 3 | import java.io.FilterReader; 4 | import java.io.IOException; 5 | import java.io.Reader; 6 | 7 | public abstract class DeferredInitFilterReader extends FilterReader { 8 | private boolean initialized; 9 | 10 | protected DeferredInitFilterReader(Reader in) { 11 | super(in); 12 | } 13 | 14 | abstract protected void initialize(); 15 | 16 | private void initializeIfNecessary() { 17 | if (!initialized) { 18 | initialize(); 19 | initialized = true; 20 | } 21 | } 22 | 23 | @Override 24 | public int read() throws IOException { 25 | initializeIfNecessary(); 26 | return super.read(); 27 | } 28 | 29 | @Override 30 | public int read(char cbuf[], int off, int len) throws IOException { 31 | initializeIfNecessary(); 32 | return super.read(cbuf, off, len); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/github/dnault/xmlpatch/internal/IoHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 David Nault and contributors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.dnault.xmlpatch.internal; 18 | 19 | import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; 20 | 21 | import java.io.File; 22 | import java.io.IOException; 23 | import java.nio.file.FileVisitResult; 24 | import java.nio.file.Files; 25 | import java.nio.file.Path; 26 | import java.nio.file.SimpleFileVisitor; 27 | import java.nio.file.attribute.BasicFileAttributes; 28 | 29 | public class IoHelper { 30 | public static void move(File from, File to) throws IOException { 31 | Files.move(from.toPath(), to.toPath(), REPLACE_EXISTING); 32 | } 33 | 34 | public static File createTempDir() throws IOException { 35 | return Files.createTempDirectory("xmlpatch-").toFile(); 36 | } 37 | 38 | public static void makeParentDirectory(File file) throws IOException { 39 | File parent = file.getParentFile(); 40 | if (!parent.exists()) { 41 | if (!parent.mkdirs()) { 42 | throw new IOException("failed to make directory: " + parent.getAbsolutePath()); 43 | } 44 | } else if (!parent.isDirectory()) { 45 | throw new IOException("not a directory: " + parent.getAbsolutePath()); 46 | } 47 | } 48 | 49 | public static void deleteDirectory(File dir) throws IOException { 50 | Files.walkFileTree(dir.toPath(), new SimpleFileVisitor() { 51 | @Override 52 | public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { 53 | Files.delete(file); 54 | return FileVisitResult.CONTINUE; 55 | } 56 | 57 | @Override 58 | public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { 59 | // try to delete the file anyway, even if its attributes could not be read, 60 | // since delete-only access is theoretically possible 61 | Files.delete(file); 62 | return FileVisitResult.CONTINUE; 63 | } 64 | 65 | @Override 66 | public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { 67 | if (exc == null) { 68 | Files.delete(dir); 69 | return FileVisitResult.CONTINUE; 70 | } else { 71 | // directory iteration failed; propagate exception 72 | throw exc; 73 | } 74 | } 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/github/dnault/xmlpatch/internal/Log.java: -------------------------------------------------------------------------------- 1 | package com.github.dnault.xmlpatch.internal; 2 | 3 | import com.github.dnault.xmlpatch.internal.logging.ConsoleLogger; 4 | import com.github.dnault.xmlpatch.internal.logging.Slf4jLogger; 5 | import com.github.dnault.xmlpatch.internal.logging.XmlPatchLogger; 6 | 7 | public class Log { 8 | private static final XmlPatchLogger logger = isSlf4jPresent() ? new Slf4jLogger() : new ConsoleLogger(); 9 | 10 | private static boolean isSlf4jPresent() { 11 | try { 12 | Class.forName("org.slf4j.Logger"); 13 | return true; 14 | } catch (ClassNotFoundException e) { 15 | return false; 16 | } 17 | } 18 | 19 | public static void debug(String s) { 20 | logger.debug(s); 21 | } 22 | 23 | public static void info(String s) { 24 | logger.info(s); 25 | } 26 | 27 | public static void warn(String s) { 28 | logger.warn(s); 29 | } 30 | 31 | public static void error(String s) { 32 | logger.error(s); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/github/dnault/xmlpatch/internal/XmlHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 David Nault and contributors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.dnault.xmlpatch.internal; 18 | 19 | import java.io.File; 20 | import java.io.FileInputStream; 21 | import java.io.FileNotFoundException; 22 | import java.io.IOException; 23 | import java.io.InputStream; 24 | import java.util.ArrayList; 25 | import java.util.HashMap; 26 | import java.util.List; 27 | import java.util.Map; 28 | 29 | import org.jdom2.Content; 30 | import org.jdom2.Document; 31 | import org.jdom2.Element; 32 | import org.jdom2.JDOMException; 33 | import org.jdom2.Namespace; 34 | import org.jdom2.Parent; 35 | import org.jdom2.input.SAXBuilder; 36 | 37 | public class XmlHelper { 38 | 39 | public static Document parse(File f) throws IOException, JDOMException { 40 | try (FileInputStream fis = new FileInputStream(f)) { 41 | return XmlHelper.parse(fis); 42 | } 43 | } 44 | 45 | public static Document parse(InputStream is) throws IOException, JDOMException { 46 | SAXBuilder builder = new SAXBuilder(); 47 | 48 | // DTD validation is makes an HTTP request and is slow. Don't need this feature, so disable it. 49 | builder.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); 50 | 51 | Document doc = builder.build(is); 52 | return doc; 53 | } 54 | 55 | public static List clone(List content) { 56 | List cloned = new ArrayList(); 57 | for (Object o : content) { 58 | cloned.add((Content) ((Content)o).clone()); 59 | } 60 | return cloned; 61 | } 62 | 63 | public static int indexOf(Parent p, Content child) { 64 | return p.getContent().indexOf(child); 65 | } 66 | 67 | public static Map getInScopeNamespaceDeclarations(Element e) { 68 | Map prefixToUri = new HashMap(); 69 | 70 | for (; e != null; e = e.getParentElement()) { 71 | putIfAbsent(prefixToUri, e.getNamespace()); 72 | 73 | for (Object o : e.getAdditionalNamespaces()) { 74 | putIfAbsent(prefixToUri, (Namespace) o); 75 | } 76 | } 77 | 78 | return prefixToUri; 79 | } 80 | 81 | @SuppressWarnings("unchecked") 82 | public static List getChildren(Element e) { 83 | return (List) e.getChildren(); 84 | } 85 | 86 | @SuppressWarnings("unchecked") 87 | public static List getChildren(Element e, String name) { 88 | return (List) e.getChildren(name); 89 | } 90 | 91 | /* NOT ATOMIC */ 92 | private static boolean putIfAbsent(Map map, K key, V value) { 93 | if (map.containsKey(key)) { 94 | return false; 95 | } 96 | map.put(key, value); 97 | return true; 98 | } 99 | 100 | private static boolean putIfAbsent(Map prefixToUri, Namespace ns) { 101 | if (ns == null) { 102 | return false; 103 | } 104 | return putIfAbsent(prefixToUri, ns.getPrefix(), ns.getURI()); 105 | } 106 | } 107 | 108 | -------------------------------------------------------------------------------- /src/main/java/com/github/dnault/xmlpatch/internal/logging/ConsoleLogger.java: -------------------------------------------------------------------------------- 1 | package com.github.dnault.xmlpatch.internal.logging; 2 | 3 | public class ConsoleLogger implements XmlPatchLogger { 4 | @Override 5 | public void info(String s) { 6 | System.out.println(s); 7 | } 8 | 9 | @Override 10 | public void warn(String s) { 11 | System.err.println("WARN: " + s); 12 | } 13 | 14 | @Override 15 | public void error(String s) { 16 | System.err.println("ERROR: " + s); 17 | } 18 | 19 | @Override 20 | public void debug(String s) { 21 | System.err.println("DEBUG: " + s); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/github/dnault/xmlpatch/internal/logging/Slf4jLogger.java: -------------------------------------------------------------------------------- 1 | package com.github.dnault.xmlpatch.internal.logging; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | public class Slf4jLogger implements XmlPatchLogger { 7 | private static Logger logger = LoggerFactory.getLogger("com.github.dnault.xmlpatch"); 8 | 9 | public void info(String s) { 10 | logger.info(s); 11 | } 12 | 13 | public void warn(String s) { 14 | logger.warn(s); 15 | } 16 | 17 | public void error(String s) { 18 | logger.error(s); 19 | } 20 | 21 | @Override 22 | public void debug(String s) { 23 | logger.debug(s); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/dnault/xmlpatch/internal/logging/XmlPatchLogger.java: -------------------------------------------------------------------------------- 1 | package com.github.dnault.xmlpatch.internal.logging; 2 | 3 | public interface XmlPatchLogger { 4 | void info(String s); 5 | 6 | void warn(String s); 7 | 8 | void error(String s); 9 | 10 | void debug(String s); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/xml-patch/commons-io-LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/xml-patch/commons-io-NOTICE.txt: -------------------------------------------------------------------------------- 1 | Apache Commons IO 2 | Copyright 2002-2012 The Apache Software Foundation 3 | 4 | This product includes software developed by 5 | The Apache Software Foundation (http://www.apache.org/). 6 | 7 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/xml-patch/jaxen-LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | $Id: LICENSE.txt 1128 2006-02-05 21:49:04Z elharo $ 3 | 4 | Copyright 2003-2006 The Werken Company. All Rights Reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are 8 | met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in the 15 | documentation and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the Jaxen Project nor the names of its 18 | contributors may be used to endorse or promote products derived 19 | from this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 22 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 23 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 24 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 25 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 26 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 27 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 28 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 29 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 30 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | */ 34 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/xml-patch/jdom-LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*-- 2 | 3 | Copyright (C) 2000-2012 Jason Hunter & Brett McLaughlin. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions 8 | are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright 11 | notice, this list of conditions, and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions, and the disclaimer that follows 15 | these conditions in the documentation and/or other materials 16 | provided with the distribution. 17 | 18 | 3. The name "JDOM" must not be used to endorse or promote products 19 | derived from this software without prior written permission. For 20 | written permission, please contact . 21 | 22 | 4. Products derived from this software may not be called "JDOM", nor 23 | may "JDOM" appear in their name, without prior written permission 24 | from the JDOM Project Management . 25 | 26 | In addition, we request (but do not require) that you include in the 27 | end-user documentation provided with the redistribution and/or in the 28 | software itself an acknowledgement equivalent to the following: 29 | "This product includes software developed by the 30 | JDOM Project (http://www.jdom.org/)." 31 | Alternatively, the acknowledgment may be graphical using the logos 32 | available at http://www.jdom.org/images/logos. 33 | 34 | THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED 35 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 36 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 37 | DISCLAIMED. IN NO EVENT SHALL THE JDOM AUTHORS OR THE PROJECT 38 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 39 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 40 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF 41 | USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 42 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 43 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 44 | OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 45 | SUCH DAMAGE. 46 | 47 | This software consists of voluntary contributions made by many 48 | individuals on behalf of the JDOM Project and was originally 49 | created by Jason Hunter and 50 | Brett McLaughlin . For more information 51 | on the JDOM Project, please see . 52 | 53 | */ 54 | 55 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/xml-patch/jdom-info.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | JDOM 4 | 2.0.6, built February 15 2015 5 | 6 | JDOM is a Java-oriented object model which models XML documents. 7 | It provides a Java-centric means of generating and manipulating 8 | XML documents. While JDOM interoperates well with existing 9 | standards such as the Simple API for XML (SAX) and the Document 10 | Object Model (DOM), it is not an abstraction layer or 11 | enhancement to those APIs. Rather, it seeks to provide a robust, 12 | light-weight means of reading and writing XML data without the 13 | complex and memory-consumptive options that current API 14 | offerings provide. 15 | 16 | 2000-2015, Jason Hunter 17 | BSD/Apache style, see LICENSE.txt 18 | See the jdom-interest mailing list at jdom.org, 19 | searchable at http://jdom.markmail.org 20 | http://www.jdom.org/ 21 | 25 | 26 | Jason Hunter (primary, co-creator) 27 | 28 | 29 | Brett McLaughlin (co-creator) 30 | 31 | 32 | Rolf Lear (primary, maintainer) 33 | 34 | 35 | Steven Gould 36 | 37 | 38 | Alex Chaffee 39 | 40 | 41 | Jon Baer 42 | 43 | 44 | Elliotte Rusty Harold 45 | 46 | 47 | Dan Schaffer 48 | 49 | 50 | Fred Trimble 51 | 52 | 53 | Jason Reid 54 | 55 | 56 | Kevin Regan 57 | 58 | 59 | Lucas Gonze 60 | 61 | 62 | Matthew Merlo 63 | 64 | 65 | Philip Nelson 66 | 67 | 68 | Wesley Biggs 69 | 70 | 71 | Wolfgang Werner 72 | 73 | 74 | Yusuf Goolamabbas 75 | 76 | 77 | Brad Huffman 78 | 79 | 80 | Victor Toni 81 | 82 | 83 | 84 | 98 | 99 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/xml-patch/joptsimple-LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License 3 | 4 | Copyright (c) 2004-2015 Paul R. Holser, Jr. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ -------------------------------------------------------------------------------- /src/main/resources/com/github/dnault/xmlpatch/antlib.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/test/java/com/github/dnault/xmlpatch/filter/multi/XmlMultiPatchTest.java: -------------------------------------------------------------------------------- 1 | package com.github.dnault.xmlpatch.filter.multi; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import java.io.Reader; 6 | import java.io.StringReader; 7 | import java.io.StringWriter; 8 | 9 | import com.github.dnault.xmlpatch.batch.AssembledPatch; 10 | import org.apache.commons.io.IOUtils; 11 | import org.jdom2.Element; 12 | import org.junit.Test; 13 | 14 | public class XmlMultiPatchTest { 15 | @Test 16 | public void testBuildFilterChain() throws Exception { 17 | StringReader source = new StringReader(""); 18 | 19 | AssembledPatch patch = new AssembledPatch(); 20 | patch.addDif(new Element("diff").setAttribute("file", "foo/bar.xml") 21 | .addContent(new Element("add").setAttribute("sel", "doc").addContent(new Element("child")))); 22 | 23 | patch.addDif(new Element("diff").setAttribute("file", "foo/bar.xml") 24 | .addContent(new Element("add").setAttribute("sel", "doc/child").addContent(new Element("grandchild")))); 25 | 26 | patch.addDif(new Element("diff").setAttribute("file", "other/bar.xml") 27 | .addContent(new Element("add").setAttribute("sel", "doc/child").addContent(new Element("grandchild")))); 28 | 29 | Reader patched = new XmlMultiPatch(source).buildFilterChain(source, "foo/bar.xml", patch); 30 | 31 | StringWriter result = new StringWriter(); 32 | 33 | IOUtils.copy(patched, result); 34 | 35 | assertEquals("\n\n", result.toString()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/github/dnault/xmlpatch/test/AddAttributeTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 David Nault and contributors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.dnault.xmlpatch.test; 18 | 19 | import static com.github.dnault.xmlpatch.test.TestHelper.DECLARATION; 20 | import static com.github.dnault.xmlpatch.test.TestHelper.EOL; 21 | import static com.github.dnault.xmlpatch.test.TestHelper.doPatch; 22 | import static com.github.dnault.xmlpatch.test.TestHelper.doPatchExpectError; 23 | import static com.github.dnault.xmlpatch.test.TestHelper.makeDiff; 24 | import static com.github.dnault.xmlpatch.test.TestHelper.makeDiffWithNamespace; 25 | import static org.junit.Assert.fail; 26 | 27 | import com.github.dnault.xmlpatch.ErrorCondition; 28 | import org.junit.Ignore; 29 | import org.junit.Test; 30 | 31 | public class AddAttributeTest { 32 | 33 | private static final String COMMON_TARGET = DECLARATION + 34 | "" + EOL + 35 | " This is a sample document" + EOL + 36 | "This is a new child"; 37 | 38 | @Test 39 | public void addAttribute() throws Exception { 40 | 41 | String diff = makeDiff("Bob"); 42 | 43 | String expectedResult = DECLARATION + 44 | "" + EOL + 45 | " This is a sample document" + EOL + 46 | "This is a new child"; 47 | 48 | doPatch(COMMON_TARGET, diff, expectedResult); 49 | } 50 | 51 | @Test 52 | public void addAttributeQualifiedWithInvalidPrefix() throws Exception { 53 | 54 | String diff = makeDiff("Bob"); 55 | 56 | doPatchExpectError(COMMON_TARGET, diff, ErrorCondition.INVALID_NAMESPACE_PREFIX); 57 | } 58 | 59 | @Test 60 | @Ignore("known failure") 61 | public void addQualifiedAttributeWithoutDeclaration() throws Exception { 62 | fail(); 63 | // todo 64 | // the reference implementation doesn't allow adding qualified attributes 65 | // unless the target document already declares the namespace. 66 | } 67 | 68 | @Test 69 | public void addQualifiedAttribute() throws Exception { 70 | 71 | String target = DECLARATION + 72 | "" + EOL + 73 | " This is a sample document" + EOL + 74 | "This is a new child"; 75 | 76 | String diff = makeDiffWithNamespace("Bob", 77 | "xmlns:y='urn:foo'"); 78 | 79 | String expectedResult = DECLARATION + 80 | "" + EOL + 81 | " This is a sample document" + EOL + 82 | "This is a new child"; 83 | 84 | doPatch(target, diff, expectedResult); 85 | } 86 | 87 | @Test 88 | public void addNamespaceDeclaration() throws Exception { 89 | 90 | String target = DECLARATION + 91 | ""; 92 | 93 | String diff = makeDiff("urn:ns:xxx"); 94 | 95 | String expectedResult = DECLARATION + 96 | ""; 97 | 98 | doPatch(target, diff, expectedResult); 99 | } 100 | 101 | @Test 102 | public void replaceAttribute() throws Exception { 103 | String target = DECLARATION + 104 | "" + EOL + 105 | " This is a sample document" + EOL + 106 | ""; 107 | 108 | String diff = makeDiff("new value"); 109 | 110 | String expectedResult = DECLARATION + 111 | "" + EOL + 112 | " This is a sample document" + EOL + 113 | ""; 114 | 115 | doPatch(target, diff, expectedResult); 116 | } 117 | 118 | @Test 119 | public void replaceAttributeWithEmptyString() throws Exception { 120 | String target = DECLARATION + 121 | "" + EOL + 122 | " This is a sample document" + EOL + 123 | ""; 124 | 125 | String diff = makeDiff(""); 126 | 127 | String expectedResult = DECLARATION + 128 | "" + EOL + 129 | " This is a sample document" + EOL + 130 | ""; 131 | 132 | doPatch(target, diff, expectedResult); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/test/java/com/github/dnault/xmlpatch/test/AddContentTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 David Nault and contributors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.dnault.xmlpatch.test; 18 | 19 | import static com.github.dnault.xmlpatch.test.TestHelper.*; 20 | 21 | import junit.framework.TestCase; 22 | 23 | import com.github.dnault.xmlpatch.ErrorCondition; 24 | import org.junit.Test; 25 | 26 | public class AddContentTest { 27 | 28 | private static final String COMMON_TARGET = DECLARATION + 29 | "" + EOL + 30 | " This is a sample document" + EOL + 31 | ""; 32 | 33 | @Test 34 | public void appendChildElement() throws Exception { 35 | String newElement = "This is a new child"; 36 | String diff = makeDiff("" + newElement + ""); 37 | 38 | String expectedResult = DECLARATION + 39 | "" + EOL + 40 | " This is a sample document" + EOL + 41 | newElement + ""; 42 | 43 | doPatch(COMMON_TARGET, diff, expectedResult); 44 | } 45 | 46 | @Test 47 | public void preppendChildElement() throws Exception { 48 | String newElement = "This is a new child"; 49 | String diff = makeDiff("" + newElement + ""); 50 | 51 | String expectedResult = DECLARATION + 52 | "" + newElement + EOL + 53 | " This is a sample document" + EOL + 54 | ""; 55 | 56 | doPatch(COMMON_TARGET, diff, expectedResult); 57 | } 58 | 59 | @Test 60 | public void addBeforeElement() throws Exception { 61 | String newElement = "This is a new child"; 62 | String diff = makeDiff("" + newElement + ""); 63 | 64 | String expectedResult = DECLARATION + 65 | "" + EOL + 66 | " " + newElement + "This is a sample document" + EOL + 67 | ""; 68 | 69 | doPatch(COMMON_TARGET, diff, expectedResult); 70 | } 71 | 72 | @Test 73 | public void addAfterElement() throws Exception { 74 | String newElement = "This is a new child"; 75 | String diff = makeDiff("" + newElement + ""); 76 | 77 | String expectedResult = DECLARATION + 78 | "" + EOL + 79 | " This is a sample document" + newElement + EOL + 80 | ""; 81 | 82 | doPatch(COMMON_TARGET, diff, expectedResult); 83 | } 84 | 85 | @Test 86 | public void addCommentBeforeElement() throws Exception { 87 | String newElement = ""; 88 | String diff = makeDiff("" + newElement + ""); 89 | 90 | String expectedResult = DECLARATION + 91 | "" + EOL + 92 | " " + newElement + "This is a sample document" + EOL + 93 | ""; 94 | 95 | doPatch(COMMON_TARGET, diff, expectedResult); 96 | } 97 | 98 | @Test 99 | public void addCommentBeforeRoot() throws Exception { 100 | String newElement = ""; 101 | String diff = makeDiff("" + newElement + ""); 102 | 103 | String expectedResult = DECLARATION + 104 | newElement + "" + EOL + 105 | " This is a sample document" + EOL + 106 | ""; 107 | 108 | doPatch(COMMON_TARGET, diff, expectedResult); 109 | } 110 | 111 | @Test 112 | public void addElementBeforeRoot() throws Exception { 113 | String newElement = ""; 114 | String diff = makeDiff("" + newElement + ""); 115 | 116 | doPatchExpectError(COMMON_TARGET, diff, ErrorCondition.INVALID_ROOT_ELEMENT_OPERATION); 117 | } 118 | 119 | @Test 120 | public void addNamespaceQualifiedElement() throws Exception { 121 | String newElement = ""; 122 | 123 | String diff = makeDiff("" + newElement + ""); 124 | 125 | String expectedResult = DECLARATION + 126 | "" + EOL + 127 | " This is a sample document" + EOL + 128 | newElement + ""; 129 | 130 | doPatch(COMMON_TARGET, diff, expectedResult); 131 | } 132 | 133 | @Test 134 | public void addNamespaceQualifiedElement2() throws Exception { 135 | String newElement = ""; 136 | 137 | String diff = TestHelper.makeDiffWithNamespace("" + newElement + "", 138 | "xmlns:x='http://example.com'"); 139 | 140 | String expectedResult = DECLARATION + 141 | "" + EOL + 142 | " This is a sample document" + EOL + 143 | ""; 144 | 145 | doPatch(COMMON_TARGET, diff, expectedResult); 146 | } 147 | 148 | @Test 149 | public void addNamespaceQualifiedElement3() throws Exception { 150 | 151 | String target = DECLARATION + 152 | "" + EOL + 153 | " This is a sample document" + EOL + 154 | ""; 155 | 156 | String newElement = ""; 157 | 158 | String diff = makeDiffWithNamespace("" + newElement + "", 159 | "xmlns:x='http://example.com'"); 160 | 161 | String expectedResult = DECLARATION + 162 | "" + EOL + 163 | " This is a sample document" + EOL + 164 | ""; 165 | 166 | doPatch(target, diff, expectedResult); 167 | } 168 | 169 | @Test 170 | public void addNamespaceQualifiedElement4() throws Exception { 171 | 172 | String target = DECLARATION + 173 | "" + EOL + 174 | " This is a sample document" + EOL + 175 | ""; 176 | 177 | String newElement = ""; 178 | 179 | String diff = makeDiffWithNamespace("" + newElement + "", 180 | "xmlns:x='http://example.com'"); 181 | 182 | String expectedResult = DECLARATION + 183 | "" + EOL + 184 | " This is a sample document" + EOL + 185 | ""; 186 | 187 | doPatch(target, diff, expectedResult); 188 | } 189 | 190 | @Test 191 | public void addUnprefixedElementUnderDefaultPrefix() throws Exception { 192 | String target = DECLARATION + 193 | "" + EOL + 194 | " This is a sample document" + EOL + 195 | ""; 196 | 197 | String newElement = ""; 198 | 199 | String diff = makeDiffWithNamespace("" + newElement + "", 200 | "xmlns:y='uri:default'"); 201 | 202 | 203 | String expectedResult = DECLARATION + 204 | "" + EOL + 205 | " This is a sample document" + EOL + 206 | ""; 207 | 208 | doPatch(target, diff, expectedResult); 209 | 210 | } 211 | 212 | @Test 213 | public void addSameNamespaceDifferentPrefix() throws Exception { 214 | 215 | String target = DECLARATION + 216 | "" + EOL + 217 | " This is a sample document" + EOL + 218 | ""; 219 | 220 | String newElement = ""; 221 | 222 | String diff = makeDiffWithNamespace("" + newElement + "", 223 | "xmlns:y='http://example.com'"); 224 | 225 | String expectedResult = DECLARATION + 226 | "" + EOL + 227 | " This is a sample document" + EOL + 228 | ""; 229 | 230 | doPatch(target, diff, expectedResult); 231 | } 232 | 233 | @Test 234 | public void addSameNamespaceDefaultPrefix() throws Exception { 235 | String target = DECLARATION + 236 | "" + EOL + 237 | " This is a sample document" + EOL + 238 | ""; 239 | 240 | String newElement = ""; 241 | 242 | String diff = makeDiffWithNamespace("" + newElement + "", 243 | "xmlns:y='http://example.com'"); 244 | 245 | String expectedResult = DECLARATION + 246 | "" + EOL + 247 | " This is a sample document" + EOL + 248 | ""; 249 | 250 | doPatch(target, diff, expectedResult); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/test/java/com/github/dnault/xmlpatch/test/DataDrivenTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 David Nault and contributors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.dnault.xmlpatch.test; 18 | 19 | import java.io.InputStream; 20 | import java.util.HashSet; 21 | import java.util.Set; 22 | 23 | import junit.framework.TestCase; 24 | import junit.framework.TestSuite; 25 | import junit.framework.Test; 26 | 27 | import com.github.dnault.xmlpatch.ErrorCondition; 28 | import com.github.dnault.xmlpatch.internal.XmlHelper; 29 | 30 | import org.jdom2.Document; 31 | import org.jdom2.Element; 32 | import org.jdom2.Comment; 33 | import org.jdom2.ProcessingInstruction; 34 | import org.jdom2.filter.Filter; 35 | import org.jdom2.filter.AbstractFilter; 36 | 37 | public class DataDrivenTest extends TestCase { 38 | 39 | private final Document target = new Document(); 40 | private final Document diff = new Document(); 41 | private final Document expectedResult = new Document(); 42 | private final ErrorCondition expectedError; 43 | 44 | private DataDrivenTest(Element e) { 45 | super(e.getAttributeValue("desc")); 46 | 47 | Filter prologFilter = new AbstractFilter() { 48 | @Override 49 | public Object filter(Object o) { 50 | if (o instanceof Element 51 | || o instanceof Comment 52 | || o instanceof ProcessingInstruction) { 53 | return o; 54 | } 55 | return null; 56 | } 57 | }; 58 | 59 | target.addContent(XmlHelper.clone(e.getChild("target").getContent(prologFilter))); 60 | diff.setRootElement((Element) e.getChild("diff").clone()); 61 | expectedResult.addContent(XmlHelper.clone(e.getChild("result").getContent(prologFilter))); 62 | 63 | String errorName = e.getChild("result").getAttributeValue("error"); 64 | expectedError = errorName == null ? null : ErrorCondition.valueOf(errorName); 65 | } 66 | 67 | public static Test suite() { 68 | String resourceName = "regression-test.xml"; 69 | 70 | TestSuite suite = new TestSuite(resourceName); 71 | 72 | InputStream is = DataDrivenTest.class.getResourceAsStream(resourceName); 73 | try { 74 | Document d = XmlHelper.parse(is); 75 | Set testNames = new HashSet(); 76 | 77 | for (Object o : d.getRootElement().getChildren()) { 78 | Element e = (Element) o; 79 | 80 | String name = e.getAttributeValue("desc"); 81 | if (!testNames.add(name)) { 82 | throw new RuntimeException("duplicate test name '" + name + "'"); 83 | } 84 | 85 | suite.addTest(new DataDrivenTest(e)); 86 | } 87 | } catch (Exception e) { 88 | throw new RuntimeException(e); 89 | } 90 | 91 | return suite; 92 | } 93 | 94 | @Override 95 | protected void runTest() throws Throwable { 96 | if (expectedError != null) { 97 | TestHelper.doPatchExpectError(target, diff, expectedError); 98 | } else { 99 | TestHelper.doPatch(target, diff, expectedResult); 100 | } 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/test/java/com/github/dnault/xmlpatch/test/ReplaceNamespaceTest.java: -------------------------------------------------------------------------------- 1 | package com.github.dnault.xmlpatch.test; 2 | 3 | import com.github.dnault.xmlpatch.ErrorCondition; 4 | import org.junit.Ignore; 5 | import org.junit.Test; 6 | 7 | import static com.github.dnault.xmlpatch.test.TestHelper.DECLARATION; 8 | import static com.github.dnault.xmlpatch.test.TestHelper.EOL; 9 | import static com.github.dnault.xmlpatch.test.TestHelper.doPatch; 10 | import static com.github.dnault.xmlpatch.test.TestHelper.doPatchExpectError; 11 | import static com.github.dnault.xmlpatch.test.TestHelper.makeDiff; 12 | 13 | @Ignore("known failure") 14 | public class ReplaceNamespaceTest { 15 | 16 | @Test 17 | public void replaceNamespaceDeclarationUri() throws Exception { 18 | String target = DECLARATION +"sample"; 19 | String diff = makeDiff("urn:new:xxx"); 20 | String expectedResult = DECLARATION + "sample"; 21 | doPatch(target, diff, expectedResult); 22 | } 23 | 24 | @Test 25 | public void replaceNamespaceUriWithEmptyString() throws Exception { 26 | String target = DECLARATION + "sample"; 27 | String diff = makeDiff(""); 28 | doPatchExpectError(target, diff, ErrorCondition.INVALID_NAMESPACE_URI); 29 | } 30 | 31 | @Test 32 | public void replaceElementsOwnNamespaceDeclarationUri() throws Exception { 33 | String target = DECLARATION +"sample"; 34 | String diff = makeDiff("urn:new:xxx"); 35 | String expectedResult = DECLARATION + "sample"; 36 | doPatch(target, diff, expectedResult); 37 | } 38 | 39 | @Test 40 | public void replaceParentsNamespaceDeclarationUri() throws Exception { 41 | String target = DECLARATION + "sample"; 42 | String diff = makeDiff("urn:new:xxx"); 43 | doPatchExpectError(target, diff, ErrorCondition.UNLOCATED_NODE); 44 | } 45 | 46 | @Test 47 | public void removeNamespaceDeclaration() throws Exception { 48 | String target = DECLARATION +""; 49 | String diff = makeDiff(""); 50 | String expectedResult = DECLARATION + ""; 51 | doPatch(target, diff, expectedResult); 52 | } 53 | 54 | @Test 55 | public void removeInUseNamespaceDeclaration() throws Exception { 56 | // spec says of the node to be removed, 57 | // "this prefix MUST NOT be associated with any node 58 | // prior to the removal of this namespace node." 59 | 60 | String target = DECLARATION + 61 | ""; 62 | String diff = makeDiff(""); 63 | doPatchExpectError(target, diff, ErrorCondition.INVALID_PATCH_DIRECTIVE); 64 | } 65 | 66 | @Test 67 | public void replaceNamespaceDeclaration() throws Exception { 68 | String target = DECLARATION + 69 | "" + EOL + 70 | " This is a sample document" + EOL + 71 | ""; 72 | 73 | String diff = makeDiff("urn:new:xxx"); 74 | 75 | String expectedResult = DECLARATION + 76 | "" + EOL + 77 | " This is a sample document" + EOL + 78 | ""; 79 | 80 | doPatch(target, diff, expectedResult); 81 | } 82 | 83 | @Test 84 | public void replaceNamespaceDeclarationError() throws Exception { 85 | String target = DECLARATION + 86 | "" + EOL + 87 | " This is a sample document" + EOL + 88 | ""; 89 | 90 | String diff = makeDiff("urn:new:xxx"); 91 | 92 | doPatchExpectError(target, diff, ErrorCondition.UNLOCATED_NODE); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/test/java/com/github/dnault/xmlpatch/test/Rfc7351Test.java: -------------------------------------------------------------------------------- 1 | package com.github.dnault.xmlpatch.test; 2 | 3 | 4 | import org.junit.Test; 5 | 6 | public class Rfc7351Test { 7 | 8 | @Test 9 | public void compatibleWithRfc7351PatchOperationNamespace() throws Exception { 10 | String target = "This is a sample document"; 11 | String patch = "\n" + 12 | " This is a new child\n" + 13 | ""; 14 | String expected = "This is a sample documentThis is a new child"; 15 | 16 | TestHelper.doPatch(target, patch, expected); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/com/github/dnault/xmlpatch/test/TestHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 David Nault and contributors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.dnault.xmlpatch.test; 18 | 19 | import static org.junit.Assert.fail; 20 | import static org.junit.Assert.assertEquals; 21 | 22 | import java.io.InputStream; 23 | import java.io.ByteArrayInputStream; 24 | import java.io.ByteArrayOutputStream; 25 | import java.io.StringReader; 26 | import java.io.StringWriter; 27 | import java.io.IOException; 28 | import java.io.UnsupportedEncodingException; 29 | 30 | import com.github.dnault.xmlpatch.ErrorCondition; 31 | import com.github.dnault.xmlpatch.PatchException; 32 | import com.github.dnault.xmlpatch.Patcher; 33 | import com.github.dnault.xmlpatch.internal.XmlHelper; 34 | 35 | import org.jdom2.Document; 36 | import org.jdom2.Element; 37 | import org.jdom2.JDOMException; 38 | import org.jdom2.input.SAXBuilder; 39 | import org.jdom2.output.XMLOutputter; 40 | 41 | import org.junit.Assert; 42 | 43 | public class TestHelper { 44 | 45 | public static final String EOL = "\n"; 46 | public static final String DECLARATION = "" + EOL; 47 | 48 | public static Document parse(String xml) throws JDOMException, IOException { 49 | SAXBuilder builder = new SAXBuilder(); 50 | Document doc = builder.build(new StringReader(xml)); 51 | return doc; 52 | } 53 | 54 | public static Document parseClasspathResource(String name) { 55 | InputStream is = TestHelper.class.getResourceAsStream(name); 56 | try { 57 | return XmlHelper.parse(is); 58 | } 59 | catch (Exception e) { 60 | throw new RuntimeException(e); 61 | } 62 | } 63 | 64 | public static String toString(Document d) throws IOException { 65 | XMLOutputter outputter = new XMLOutputter(); 66 | StringWriter w = new StringWriter(); 67 | outputter.output(d, w); 68 | return w.toString(); 69 | } 70 | 71 | public static String toString(Element e) throws IOException { 72 | XMLOutputter outputter = new XMLOutputter(); 73 | StringWriter w = new StringWriter(); 74 | outputter.output(e, w); 75 | return w.toString(); 76 | } 77 | 78 | public static void assertXmlEquals(Document expected, Document actual) throws IOException { 79 | Assert.assertEquals(toString(expected), toString(actual)); 80 | } 81 | 82 | public static void doPatch(Document target, Document diff, Document expectedResult) throws Exception { 83 | doPatch(toString(target),toString(diff),toString(expectedResult)); 84 | } 85 | 86 | public static void doPatchExpectError(Document target, Document diff, ErrorCondition expectedError) throws Exception { 87 | doPatchExpectError(toString(target), toString(diff), expectedError); 88 | } 89 | 90 | public static void doPatch(String target, String diff, String expectedResult) throws Exception { 91 | ByteArrayOutputStream os = new ByteArrayOutputStream(); 92 | Patcher.patch(asStream(target), asStream(diff), os); 93 | 94 | Document expected = XmlHelper.parse(asStream(expectedResult)); 95 | Document actual = XmlHelper.parse(new ByteArrayInputStream(os.toByteArray())); 96 | assertXmlEquals(expected, actual); 97 | } 98 | 99 | public static void doPatchExpectError(String target, String diff, ErrorCondition expectedError) throws Exception { 100 | ByteArrayOutputStream os = new ByteArrayOutputStream(); 101 | try { 102 | Patcher.patch(asStream(target), asStream(diff), os); 103 | fail("expected error condition: " + expectedError.name() + 104 | "\nbut got:\n" + os.toString("UTF-8")); 105 | } catch (PatchException e) { 106 | if (!expectedError.equals(e.getErrorCondition())) { 107 | e.printStackTrace(); 108 | assertEquals(expectedError, e.getErrorCondition()); 109 | } 110 | } 111 | } 112 | 113 | private static InputStream asStream(String s) { 114 | try { 115 | return new ByteArrayInputStream(s.getBytes("UTF-8")); 116 | } 117 | catch (UnsupportedEncodingException e) { 118 | throw new RuntimeException(e); 119 | } 120 | } 121 | 122 | public static String makeDiff(String s) { 123 | return DECLARATION + "" + s + ""; 124 | } 125 | public static String makeDiffWithNamespace(String s, String namespace) { 126 | return DECLARATION + "" + s + ""; 127 | } 128 | 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/test/java/com/github/dnault/xmlpatch/test/XmlHelperTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 David Nault and contributors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.dnault.xmlpatch.test; 18 | 19 | import static com.github.dnault.xmlpatch.internal.XmlHelper.getInScopeNamespaceDeclarations; 20 | import static org.junit.Assert.assertEquals; 21 | 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | 25 | import org.jdom2.Document; 26 | import org.jdom2.Element; 27 | import org.junit.Test; 28 | 29 | public class XmlHelperTest { 30 | 31 | @Test 32 | public void testGetInScopeNamespaceDeclarations() throws Exception { 33 | String xml = "" + 34 | "" + 35 | "" + 36 | ""; 37 | 38 | Document doc = TestHelper.parse(xml); 39 | Element a = doc.getRootElement(); 40 | Element b = getFirstChild(a); 41 | Element c = getFirstChild(b); 42 | Element d = getFirstChild(c); 43 | 44 | Map prefixToURI = new HashMap(); 45 | prefixToURI.put("", ""); 46 | prefixToURI.put("one", "uri:one"); 47 | prefixToURI.put("two", "uri:two"); 48 | assertEquals(prefixToURI, getInScopeNamespaceDeclarations(a)); 49 | 50 | prefixToURI.put("", "uri:default"); 51 | assertEquals(prefixToURI, getInScopeNamespaceDeclarations(b)); 52 | 53 | prefixToURI.put("two", "uri:anothertwo"); 54 | assertEquals(prefixToURI, getInScopeNamespaceDeclarations(c)); 55 | assertEquals(prefixToURI, getInScopeNamespaceDeclarations(d)); 56 | } 57 | 58 | private Element getFirstChild(Element e) { 59 | return (Element) e.getChildren().get(0); 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /src/test/resources/com/github/dnault/xmlpatch/test/regression-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | This is a sample document 16 | 17 | 18 | 19 | 20 | This is a new child 21 | 22 | 23 | 24 | 25 | This is a sample document 26 | This is a new child 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | hello 79 | 80 | 81 | 82 | 83 | 84 | urn:ns:xxx 85 | 86 | 87 | 88 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | sample 104 | urn:new:xxx 105 | 106 | 107 | 108 | 109 | sample 110 | one 111 | sample 112 | 113 | 114 | 115 | 116 | 2 117 | 118 | 119 | 120 | 121 | sample 122 | 123 | 124 | 125 | 126 | 127 | sample 128 | replacement 129 | replacement 130 | 131 | 132 | 133 | sample 134 | 135 | 136 | 137 | 138 | 139 | sample 140 | 141 | 142 | 143 | 144 | 145 | sample 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | sample 158 | 159 | 160 | 161 | 162 | 163 | sample 164 | 165 | sample 166 | 167 | 168 | 169 | sample 170 | 171 | sample 172 | 173 | 174 | 175 | sample 176 | replacement 177 | replacement 178 | 179 | 180 | 181 | sample 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | hello 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | hello 273 | 274 | 275 | 276 | 277 | --------------------------------------------------------------------------------