├── .github └── workflows │ ├── codecov.yml │ └── maven.yml ├── .gitignore ├── CHANGES.md ├── LICENSE-2.0.txt ├── README.md ├── pom.xml └── src ├── main └── java │ └── com │ └── jamesmurty │ └── utils │ ├── BaseXMLBuilder.java │ ├── NamespaceContextImpl.java │ ├── XMLBuilder.java │ ├── XMLBuilder2.java │ └── XMLBuilderRuntimeException.java └── test └── java └── com └── jamesmurty └── utils ├── BaseXMLBuilderTests.java ├── TestXMLBuilder.java ├── TestXMLBuilder2.java └── external.txt /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Codecov 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | push: 9 | pull_request: 10 | 11 | 12 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 13 | jobs: 14 | # This workflow contains a single job called "build" 15 | build: 16 | # The type of runner that the job will run on 17 | runs-on: ubuntu-latest 18 | 19 | # Steps represent a sequence of tasks that will be executed as part of the job 20 | steps: 21 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 22 | - uses: actions/checkout@v2 23 | 24 | - name: Run cobertura 25 | run: mvn cobertura:cobertura 26 | 27 | - name: Upload report 28 | run: bash <(curl -s https://codecov.io/bash) 29 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Build 5 | 6 | on: 7 | push: 8 | pull_request: 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | java: [7, 8, 9.0.x, 10, 11, 12, 13] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | 22 | - name: Set up JDK 23 | uses: actions/setup-java@v1 24 | with: 25 | java-version: ${{ matrix.java }} 26 | 27 | - name: Clean Build 28 | run: mvn clean 29 | 30 | - name: Build with Maven 31 | run: mvn -B package --file pom.xml -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.svn 2 | /.classpath 3 | /.project 4 | /.settings 5 | /.envrc 6 | /mvn 7 | /bin 8 | /target 9 | /java-xmlbuilder 10 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | Release Notes for java-xmlbuilder 2 | ================================= 3 | 4 | Version 1.3 - 8 July 2020 5 | ------------------------- 6 | 7 | Fixes: 8 | 9 | * Update source version from 1.5 to 1.7 to work with Java versions 11+ which no 10 | longer supports `javax.annotation` and because 6 no longer builds with Maven. 11 | 12 | * Tweaks to pom.xml to get Maven builds to work with Java 12. 13 | 14 | * Replace references to my defunct website with working alternatives. 15 | 16 | Version 1.2 - 1 September 2017 17 | ------------------------------ 18 | 19 | Fixes: 20 | 21 | * Prevent XML External Entity (XXE) injection attacks by disabling parsing of 22 | general and parameter external entities by default (#6). External entities 23 | are now only parsed if this feature is explicitly enabled by passing a boolean 24 | flag value to the #create and #parse methods. 25 | WARNING: This will break code that expects external entities to be parsed. 26 | 27 | Enhancements: 28 | 29 | * Permit users to disable namespace-awareness in the underlying 30 | DocumentBuilderFactory when constructing the builder with extended `create()` 31 | and `parse()` methods. Namespace awareness is enabled by default unless you 32 | use the more explicit versions of these methods that take additional 33 | `enableExternalEntities` and `isNamespaceAware` parameters. 34 | 35 | Version 1.1 - 22 July 2014 36 | -------------------------- 37 | 38 | Added a new `XMLBuilder2` implementation that avoids checked exceptions in the 39 | API and throws runtime exceptions instead. This should make the library much 40 | more pleasant to use, and your code much cleaner, in situations where low-level 41 | exceptions are unlikely -- which is probably most situations where you would 42 | use this library. 43 | 44 | For example when creating a new document with the `#create` method, instead of 45 | needing to explicitly catch the unlikely `ParserConfigurationException`, if you 46 | use `XMLBuilder2` this exception automatically gets wrapped in the new 47 | `XMLBuilderRuntimeException` class and can be left to propagate out. 48 | 49 | Aside from the removal of checked exceptions, `XMLBuilder2` has the same API as 50 | the original `XMLBuilder` and should therefore be a drop-in replacement in 51 | existing code. 52 | 53 | For further discussion and rationale see: 54 | https://github.com/jmurty/java-xmlbuilder/issues/4 55 | 56 | Version 1.0 - 6 March 2014 57 | -------------------------- 58 | 59 | Jumped version number from 0.7 to 1.0 to better reflect this project's age 60 | and stability, as well as to celebrate the move to GitHub. 61 | 62 | * Migrated project from 63 | [Google Code](https://code.google.com/p/java-xmlbuilder/) to 64 | [GitHub](https://github.com/jmurty/java-xmlbuilder). Whew, that's better! 65 | * Test cases for edge-case issues and questions reported by users. 66 | * Add `parse` methods to parse XML directly from a String or File. 67 | * Allow sub-elements to be added when they will have Text node siblings, 68 | provided they are whitespace-only text nodes. 69 | * Add `stripWhitespaceOnlyTextNodes` method to strip document of 70 | whitespace-only text nodes. 71 | 72 | Version 0.6 - 28 April 2013 73 | --------------------------- 74 | 75 | * Add Apache 2.0 LICENSE file to be more explicit about licensing. 76 | * Update iharder Base64 library to 2.3.8 and source via Maven dependency, 77 | rather than including the source directly. 78 | * New methods 'document' to return builder for root Document node, 79 | and 'insertInstruction' to insert processing instruction before current node. 80 | * Fail fast if null text value is provided to `text` methods. 81 | 82 | Version 0.5 - 17 January 2013 83 | ----------------------------- 84 | 85 | * Extra CDATA methods to create nodes with strings without any Base64-encoding. 86 | * Serialization of document sub-trees via `toWriter`/`elementAsString` methods. 87 | * Namespace support when building document, and when performing xpath queries 88 | * Perform arbitrary XPath queries using the `xpathQuery` method. 89 | * New `elementBefore()` methods insert a new XML node before the current node. 90 | * Added `text()` method with boolean flag as second parameter to replace a 91 | node's text value, rather than always appending text. 92 | 93 | Version 0.4 - 26 October 2010 94 | ----------------------------- 95 | 96 | * Fix for ClassCastException that could occur if user attempts to traverse 97 | beyond the current document's root element using `up()` methods. 98 | * Added method `importXMLBuilder` method to import another XMLBuilder document 99 | at the current document position. 100 | 101 | Version 0.3 - 2 July 2010 102 | ------------------------- 103 | 104 | * First release to Maven repository. 105 | * Parse existing XML documents with `parse` method. 106 | * Find specific nodes in document with an XPath with `xpathFind`. 107 | * Added JUnit tests 108 | 109 | Version 0.2 - 6 January 2009 110 | ---------------------------- 111 | 112 | * Updated CDATA methods (`cdata`, `data`, `d`) to automatically Base64-encode 113 | text values using Base64 implementation from http://iharder.net/base64 114 | 115 | Version 0.1 - 14 December 2008 116 | ------------------------------ 117 | 118 | * Initial public release 119 | -------------------------------------------------------------------------------- /LICENSE-2.0.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | java-xmlbuilder 2 | =============== 3 | 4 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.jamesmurty.utils/java-xmlbuilder/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.jamesmurty.utils/java-xmlbuilder) 5 | 6 | ![Build](https://github.com/jmurty/java-xmlbuilder/workflows/Build/badge.svg) ![Codecov](https://github.com/jmurty/java-xmlbuilder/workflows/Codecov/badge.svg) 7 | 8 | [![codecov](https://codecov.io/gh/jmurty/java-xmlbuilder/branch/master/graph/badge.svg)](https://codecov.io/gh/jmurty/java-xmlbuilder) 9 | 10 | XML Builder is a utility that allows simple XML documents to be constructed 11 | using relatively sparse Java code. 12 | 13 | It allows for quick and painless creation of XML documents where you might 14 | otherwise be tempted to use concatenated strings, and where you would rather 15 | not face the tedium and verbosity of coding with 16 | [JAXP](http://jaxp.dev.java.net/). 17 | 18 | Internally, XML Builder uses JAXP to build a standard W3C Document model (DOM) 19 | that you can easily export as a string, or access directly to manipulate 20 | further if you have special requirements. 21 | 22 | ### License 23 | 24 | [Apache License Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) 25 | 26 | XMLBuilder versus XMLBuilder2 27 | ----------------------------- 28 | 29 | Since version 1.1 this library provides two builder implementations and APIs: 30 | 31 | * `XMLBuilder` – the original API – follows standard Java practice of 32 | re-throwing lower level checked exceptions when you do things like create a 33 | new document. 34 | You must explicitly `catch` these checked exceptions in your codebase, even 35 | though they are unlikely to occur in tested code. 36 | * `XMLBuilder2` is a newer API that removes checked exceptions altogether, and 37 | will instead wrap and propagate lower level exceptions in an unchecked 38 | `XMLBuilderRuntimeException`. 39 | Use this class if you don't like the code mess or overhead of try/catching 40 | many low-level exceptions that are unlikely to occur in practice. 41 | 42 | Both these versions work identically apart from the handling of errors, so you 43 | can use whichever version you prefer or "upgrade" from one to the other in 44 | existing code. 45 | 46 | Quick Example 47 | ------------- 48 | 49 | Easily build XML documents using code structured like the final document. 50 | 51 | This code: 52 | 53 | ```java 54 | XMLBuilder2 builder = XMLBuilder2.create("Projects") 55 | .e("java-xmlbuilder").a("language", "Java").a("scm","SVN") 56 | .e("Location").a("type", "URL") 57 | .t("http://code.google.com/p/java-xmlbuilder/") 58 | .up() 59 | .up() 60 | .e("JetS3t").a("language", "Java").a("scm","CVS") 61 | .e("Location").a("type", "URL") 62 | .t("http://jets3t.s3.amazonaws.com/index.html"); 63 | ``` 64 | 65 | Produces this XML document: 66 | 67 | ```xml 68 | 69 | 70 | 71 | http://code.google.com/p/java-xmlbuilder/ 72 | 73 | 74 | http://jets3t.s3.amazonaws.com/index.html 75 | 76 | 77 | ``` 78 | 79 | Getting Started 80 | --------------- 81 | 82 | See further example usage below and in the 83 | [JavaDoc documentation](http://s3.james.murty.co/java-xmlbuilder/index.html). 84 | 85 | Download a Jar file containing the latest version 86 | [java-xmlbuilder-1.3.jar](http://s3.james.murty.co/java-xmlbuilder/java-xmlbuilder-1.3.jar). 87 | 88 | Maven users can add this project as a dependency with the following additions 89 | to a POM.xml file: 90 | 91 | ```maven 92 | 93 | . . . 94 | 95 | com.jamesmurty.utils 96 | java-xmlbuilder 97 | 1.3 98 | 99 | . . . 100 | 101 | ``` 102 | 103 | How to use the XMLBuilder 104 | ------------------------- 105 | 106 | Read below for examples that show how you would use the XMLBuilder utility to 107 | create and manipulate XML documents like the following: 108 | 109 | ```xml 110 | 111 | 112 | 113 | http://code.google.com/p/java-xmlbuilder/ 114 | 115 | 116 | http://jets3t.s3.amazonaws.com/index.html 117 | 118 | 119 | ``` 120 | 121 | ### Create a New XML Document 122 | 123 | To begin, you create a new builder and XML document by specifying the name of 124 | the document's root element. See the parsing methods below if you want to start 125 | with an existing XML document. 126 | 127 | ```java 128 | XMLBuilder builder = XMLBuilder.create("Projects"); 129 | ``` 130 | 131 | The XMLBuilder object returned by the `create` method, and by all other XML 132 | manipulation methods, provides methods that you can use to add more nodes to 133 | the document. For example, to add the `java-xmlbuilder` and `JetS3t` nodes to 134 | the root element you could do the following. 135 | 136 | ```java 137 | XMLBuilder e1 = builder.element("java-xmlbuilder"); 138 | XMLBuilder e2 = builder.element("JetS3t"); 139 | ``` 140 | 141 | And to add attributes or further sub-elements to the two new elements, you 142 | could call the appropriate methods on the variables assigned to each new node 143 | like so: 144 | 145 | ```java 146 | e1.attribute("language", "Java"); 147 | e1.attribute("scm", "SVN"); 148 | ``` 149 | 150 | This is straight-forward enough, but it is far more verbose than necessary 151 | because the code does not take advantage of XMLBuilder's method-chaining 152 | feature. 153 | 154 | ### Method Chaining 155 | 156 | Every XMLBuilder method that adds something to the XML document will return an 157 | XMLBuilder object that represents either a newly-added element, or the element 158 | to which something has been added. This feature means that you can chain 159 | together many method calls without the need to assign intermediate objects to 160 | variables. 161 | 162 | With this in mind, here is code that performs the same job as the code above 163 | without any unnecessary variables. 164 | 165 | ```java 166 | XMLBuilder builder = XMLBuilder.create("Projects") 167 | .element("java-xmlbuilder") 168 | .attribute("language", "Java") 169 | .attribute("scm", "SVN") 170 | .element("Location") 171 | .up() 172 | .up() 173 | .element("JetS3t"); 174 | ``` 175 | 176 | There are two important things to notice in the code above: 177 | 178 | * When you add a new element to the document with the `element` method, the 179 | XMLBuilder node returned will represent that new element. If you invoke 180 | methods on this node, attributes and elements will be added to the new node 181 | rather than to the document's root. 182 | * Once you have finished adding items to a new element, you can call the 183 | `up()` method to retrieve the XMLBuilder node that represents the parent of 184 | the current node. If you balance every call to `element()` with a call to 185 | `up()`, you can write code that closely resembles the structure of the XML 186 | document you are creating. 187 | 188 | ### Shorthand Methods 189 | 190 | To make your XML building code even shorter and easier to type, there are 191 | shorthand synonyms for every XML manipulation method. Instead of calling 192 | `element()` you can use the `elem()` or `e()` methods, and instead of typing 193 | `attribute()` you can use `attr()` or `a()`. 194 | 195 | Here is the complete code to build our example XML document using shorthand 196 | methods. 197 | 198 | ```java 199 | XMLBuilder builder = XMLBuilder.create("Projects") 200 | .e("java-xmlbuilder") 201 | .a("language", "Java") 202 | .a("scm","SVN") 203 | .e("Location") 204 | .a("type", "URL") 205 | .t("http://code.google.com/p/java-xmlbuilder/") 206 | .up() 207 | .up() 208 | .e("JetS3t") 209 | .a("language", "Java") 210 | .a("scm","CVS") 211 | .e("Location") 212 | .a("type", "URL") 213 | .t("http://jets3t.s3.amazonaws.com/index.html"); 214 | ``` 215 | 216 | The following methods are available for adding items to the XML document: 217 | 218 | | XML Node | Methods | 219 | | -------------------- | -------------------------- | 220 | | Element | `element`, `elem`, `e` | 221 | | Attribute | `attribute`, `attr`, `a` | 222 | | Text (Element Value) | `text`, `t` | 223 | | CDATA | `cdata`, `data`, `d` | 224 | | Comment | `comment`, `cmnt`, `c` | 225 | | Process Instruction | `instruction`, `inst`, `i` | 226 | | Reference | `reference`, `ref`, `r` | 227 | 228 | ### Output 229 | 230 | XMLBuilder includes two convenient methods for outputting a document. 231 | 232 | You can use the `toWriter` method to print the document to an output stream or 233 | file: 234 | ```java 235 | PrintWriter writer = new PrintWriter(new FileOutputStream("projects.xml")); 236 | builder.toWriter(writer, outputProperties); 237 | ``` 238 | 239 | Or you can convert the document straight to a text string: 240 | ```java 241 | builder.asString(outputProperties); 242 | ``` 243 | 244 | Both of these output methods take an `outputProperties` parameter that you can 245 | use to control how the output is generated. Any output properties you provide 246 | are forwarded to the underlying Transformer object that is used to serialize 247 | the XML document. 248 | 249 | You might specify any non-standard properties like so: 250 | 251 | ```java 252 | Properties outputProperties = new Properties(); 253 | 254 | // Explicitly identify the output as an XML document 255 | outputProperties.put(javax.xml.transform.OutputKeys.METHOD, "xml"); 256 | 257 | // Pretty-print the XML output (doesn't work in all cases) 258 | outputProperties.put(javax.xml.transform.OutputKeys.INDENT, "yes"); 259 | 260 | // Get 2-space indenting when using the Apache transformer 261 | outputProperties.put("{http://xml.apache.org/xslt}indent-amount", "2"); 262 | 263 | // Omit the XML declaration header 264 | outputProperties.put( 265 | javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes"); 266 | ``` 267 | 268 | If you do not wish to change the default properties for your output, you can 269 | provide a null value for `outputProperties`. 270 | 271 | ### Accessing the Underlying Document 272 | 273 | Because XMLBuilder merely acts as a layer on top of the standard JAXP XML 274 | document building tools, you can easily access the underlying Element or 275 | Document objects if you need to manipulate them in ways that XMLBuilder does 276 | not allow. 277 | 278 | To obtain the Element represented by any given XMLBuilder node: 279 | ```java 280 | org.w3c.dom.Element element = xmlBuilderNode.getElement(); 281 | ``` 282 | 283 | To obtain the entire XML document: 284 | ```java 285 | org.w3c.dom.Document doc = builder.getDocument(); 286 | ``` 287 | 288 | You can also use the `root()` method to quickly obtain the builder object that 289 | represents the document's root element, no matter deep an element hierarchy 290 | your code has built: 291 | 292 | ```java 293 | org.w3c.dom.Element rootElement = 294 | XMLBuilder.create("This") 295 | .e("Element") 296 | .e("Hierarchy") 297 | .e("Is") 298 | .e("Really") 299 | .e("Very") 300 | .e("Deep") 301 | .e("Indeed") 302 | .root().getElement(); 303 | ``` 304 | 305 | ### Parse XML 306 | 307 | If you already have an XML document to which you need to add nodes or 308 | attributes, you can create a new XMLBuilder instance by parsing an 309 | `InputSource`, `String`, or `File`: 310 | 311 | ```java 312 | XMLBuilder builder = XMLBuilder.parse(YOUR_XML_DOCUMENT_STRING); 313 | ``` 314 | 315 | Parsing an existing document will produce an XMLBuilder object pointing at the 316 | document's root Element node. If you add elements or attributes to this builder 317 | object, they will be added to the document's root element. 318 | 319 | If you need to add nodes elsewhere in the parsed document, you will need to 320 | find the correct location in the document using XPath statements. 321 | 322 | ### Find Nodes with XPath 323 | 324 | To add nodes at a specific point in an XML document, you can use XPath to 325 | obtain an XMLBuilder at the correct location. The `XMLBuilder#xpathFind` method 326 | takes an XPath query string and returns a builder object located at the *first* 327 | Element that matches the query. 328 | 329 | ```java 330 | XMLBuilder firstLocationBuilder = builder.xpathFind("//Location"); 331 | ``` 332 | 333 | Note that the XPath query provided to this method *must resolve to at least one 334 | Element node*. If the query does not match any nodes, or if the first match is 335 | anything other than an Element, the method will throw an 336 | XPathExpressionException. 337 | 338 | Like all other XMLBuilder methods, this method can be easily chained to others 339 | when adding nodes. Here is an example that adds a second element, `Location2`, 340 | inside the `JetS3t` element of our example document. 341 | 342 | ```java 343 | builder.xpathFind("//JetS3t").elem("Location2").attr("type", "Testing"); 344 | ``` 345 | 346 | To produce: 347 | 348 | ```xml 349 | 350 | 351 | http://code.google.com/p/java-xmlbuilder/ 352 | 353 | 354 | http://jets3t.s3.amazonaws.com/index.html 355 | 356 | 357 | 358 | ``` 359 | 360 | ### Configuring advanced features 361 | 362 | When creating or parsing a document you can enable and disable advanced 363 | features by using the more explicit versions of the `parse()` and `create()` 364 | constructors. 365 | 366 | You can: 367 | 368 | * use the `enableExternalEntities` flag to enable or disable external entities. 369 | NOTE: you should leave these disabled, as they are by default, unless you 370 | really need them because they open you to XML External Entity (XXE) injection 371 | attacks. 372 | * use the `isNamespaceAware` flag to enable or disable namespace awareness in 373 | the underlying `DocumentBuilderFactory`. 374 | 375 | 376 | Release History 377 | --------------- 378 | 379 | See this project's version history in 380 | [CHANGES.md](https://github.com/jmurty/java-xmlbuilder/blob/master/CHANGES.md) 381 | 382 | This project was previously hosted on Google Code at 383 | [https://code.google.com/p/java-xmlbuilder/](https://code.google.com/p/java-xmlbuilder/). 384 | Please refer to this old location for historical issue reports and user 385 | questions. 386 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | com.jamesmurty.utils 4 | java-xmlbuilder 5 | jar 6 | 7 | 1.4-SNAPSHOT 8 | java-xmlbuilder 9 | XML Builder is a utility that creates simple XML documents using relatively sparse Java code 10 | https://github.com/jmurty/java-xmlbuilder 11 | 12 | 13 | 1.7 14 | 1.7 15 | 16 | 17 | 18 | 19 | Apache License, Version 2.0 20 | http://www.apache.org/licenses/LICENSE-2.0 21 | repo 22 | 23 | 24 | 25 | 26 | scm:git:github.com/jmurty/java-xmlbuilder.git 27 | scm:git:git@github.com:jmurty/java-xmlbuilder.git 28 | https://github.com/jmurty/java-xmlbuilder 29 | HEAD 30 | 31 | 32 | 33 | 34 | jmurty 35 | James Murty 36 | https://github.com/jmurty 37 | 38 | developer 39 | 40 | 41 | 42 | 43 | 44 | 45 | net.iharder 46 | base64 47 | 2.3.8 48 | compile 49 | 50 | 51 | 52 | junit 53 | junit 54 | 4.13.1 55 | test 56 | 57 | 58 | 59 | 60 | org.sonatype.oss 61 | oss-parent 62 | 7 63 | 64 | 65 | 66 | 67 | 68 | 69 | org.apache.maven.plugins 70 | maven-release-plugin 71 | 2.5 72 | 73 | v@{project.version} 74 | false 75 | -Dgpgsign=true 76 | 77 | 78 | 79 | 80 | org.apache.maven.plugins 81 | maven-source-plugin 82 | 2.2.1 83 | 84 | 85 | attach-sources 86 | 87 | jar 88 | 89 | 90 | 91 | 92 | 93 | 94 | org.apache.maven.plugins 95 | maven-javadoc-plugin 96 | 2.9 97 | 98 | 7 99 | 100 | 101 | 102 | attach-javadocs 103 | 104 | jar 105 | 106 | 107 | 108 | 109 | 110 | 111 | org.codehaus.mojo 112 | cobertura-maven-plugin 113 | 2.7 114 | 115 | 116 | html 117 | xml 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | release-sign-artifacts 129 | 130 | 131 | gpgsign 132 | true 133 | 134 | 135 | 136 | 137 | 138 | org.apache.maven.plugins 139 | maven-gpg-plugin 140 | 1.4 141 | 142 | 143 | sign-artifacts 144 | verify 145 | 146 | sign 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2008-2020 James Murty (github.com/jmurty) 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 | * This code is available from the GitHub code repository at: 18 | * https://github.com/jmurty/java-xmlbuilder 19 | */ 20 | package com.jamesmurty.utils; 21 | 22 | import java.io.IOException; 23 | import java.io.StringWriter; 24 | import java.io.Writer; 25 | import java.util.Map.Entry; 26 | import java.util.Properties; 27 | 28 | import javax.xml.namespace.NamespaceContext; 29 | import javax.xml.namespace.QName; 30 | import javax.xml.parsers.DocumentBuilder; 31 | import javax.xml.parsers.DocumentBuilderFactory; 32 | import javax.xml.parsers.FactoryConfigurationError; 33 | import javax.xml.parsers.ParserConfigurationException; 34 | import javax.xml.transform.Transformer; 35 | import javax.xml.transform.TransformerException; 36 | import javax.xml.transform.TransformerFactory; 37 | import javax.xml.transform.dom.DOMSource; 38 | import javax.xml.transform.stream.StreamResult; 39 | import javax.xml.xpath.XPath; 40 | import javax.xml.xpath.XPathConstants; 41 | import javax.xml.xpath.XPathExpression; 42 | import javax.xml.xpath.XPathExpressionException; 43 | import javax.xml.xpath.XPathFactory; 44 | 45 | import net.iharder.Base64; 46 | 47 | import org.w3c.dom.Document; 48 | import org.w3c.dom.Element; 49 | import org.w3c.dom.Node; 50 | import org.w3c.dom.NodeList; 51 | import org.xml.sax.InputSource; 52 | import org.xml.sax.SAXException; 53 | 54 | /** 55 | * Base abstract class for all XML Builder implementations. 56 | * Most of the work is done here. 57 | * 58 | * @author jmurty 59 | */ 60 | public abstract class BaseXMLBuilder { 61 | 62 | /** 63 | * A DOM Document that stores the underlying XML document operated on by 64 | * BaseXMLBuilder instances. This document object belongs to the root node 65 | * of a document, and is shared by this node with all other BaseXMLBuilder 66 | * instances via the {@link #getDocument()} method. 67 | * This instance variable must only be created once, by the root node for 68 | * any given document. 69 | */ 70 | private Document xmlDocument = null; 71 | 72 | /** 73 | * The underlying node represented by this builder node. 74 | */ 75 | private Node xmlNode = null; 76 | 77 | /** 78 | * If true, the builder will raise an {@link XMLBuilderRuntimeException} 79 | * if external general and parameter entities cannot be explicitly enabled 80 | * or disabled. 81 | */ 82 | public static boolean failIfExternalEntityParsingCannotBeConfigured = true; 83 | 84 | /** 85 | * Construct a new builder object that wraps the given XML document. 86 | * This constructor is for internal use only. 87 | * 88 | * @param xmlDocument 89 | * an XML document that the builder will manage and manipulate. 90 | */ 91 | protected BaseXMLBuilder(Document xmlDocument) { 92 | this.xmlDocument = xmlDocument; 93 | this.xmlNode = xmlDocument.getDocumentElement(); 94 | } 95 | 96 | /** 97 | * Construct a new builder object that wraps the given XML document and node. 98 | * This constructor is for internal use only. 99 | * 100 | * @param myNode 101 | * the XML node that this builder node will wrap. This node may 102 | * be part of the XML document, or it may be a new element that is to be 103 | * added to the document. 104 | * @param parentNode 105 | * If not null, the given myElement will be appended as child node of the 106 | * parentNode node. 107 | */ 108 | protected BaseXMLBuilder(Node myNode, Node parentNode) { 109 | this.xmlNode = myNode; 110 | if (myNode instanceof Document) { 111 | this.xmlDocument = (Document) myNode; 112 | } else { 113 | this.xmlDocument = myNode.getOwnerDocument(); 114 | } 115 | if (parentNode != null) { 116 | parentNode.appendChild(myNode); 117 | } 118 | } 119 | 120 | /** 121 | * Explicitly enable or disable the 'external-general-entities' and 122 | * 'external-parameter-entities' features of the underlying 123 | * DocumentBuilderFactory. 124 | * 125 | * TODO This is a naive approach that simply tries to apply all known 126 | * feature name/URL values in turn until one succeeds, or none do. 127 | * 128 | * @param factory 129 | * factory which will have external general and parameter entities enabled 130 | * or disabled. 131 | * @param enableExternalEntities 132 | * if true external entities will be explicitly enabled, otherwise they 133 | * will be explicitly disabled. 134 | */ 135 | protected static void enableOrDisableExternalEntityParsing( 136 | DocumentBuilderFactory factory, boolean enableExternalEntities) 137 | { 138 | // Feature list drawn from: 139 | // https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing 140 | 141 | /* Enable or disable external general entities */ 142 | String[] externalGeneralEntitiesFeatures = { 143 | // General 144 | "http://xml.org/sax/features/external-general-entities", 145 | // Xerces 1 146 | "http://xerces.apache.org/xerces-j/features.html#external-general-entities", 147 | // Xerces 2 148 | "http://xerces.apache.org/xerces2-j/features.html#external-general-entities", 149 | }; 150 | boolean success = false; 151 | for (String feature: externalGeneralEntitiesFeatures) { 152 | try { 153 | factory.setFeature(feature, enableExternalEntities); 154 | success = true; 155 | break; 156 | } catch (ParserConfigurationException e) { 157 | } 158 | } 159 | if (!success && failIfExternalEntityParsingCannotBeConfigured) { 160 | throw new XMLBuilderRuntimeException( 161 | new ParserConfigurationException( 162 | "Failed to set 'external-general-entities' feature to " 163 | + enableExternalEntities)); 164 | } 165 | 166 | /* Enable or disable external parameter entities */ 167 | String[] externalParameterEntitiesFeatures = { 168 | // General 169 | "http://xml.org/sax/features/external-parameter-entities", 170 | // Xerces 1 171 | "http://xerces.apache.org/xerces-j/features.html#external-parameter-entities", 172 | // Xerces 2 173 | "http://xerces.apache.org/xerces2-j/features.html#external-parameter-entities", 174 | }; 175 | success = false; 176 | for (String feature: externalParameterEntitiesFeatures) { 177 | try { 178 | factory.setFeature(feature, enableExternalEntities); 179 | success = true; 180 | break; 181 | } catch (ParserConfigurationException e) { 182 | } 183 | } 184 | if (!success && failIfExternalEntityParsingCannotBeConfigured) { 185 | throw new XMLBuilderRuntimeException( 186 | new ParserConfigurationException( 187 | "Failed to set 'external-parameter-entities' feature to " 188 | + enableExternalEntities)); 189 | } 190 | } 191 | 192 | /** 193 | * Construct an XML Document with a default namespace with the given 194 | * root element. 195 | * 196 | * @param name 197 | * the name of the document's root element. 198 | * @param namespaceURI 199 | * default namespace URI for document, ignored if null or empty. 200 | * @param enableExternalEntities 201 | * enable external entities; beware of XML External Entity (XXE) injection. 202 | * @param isNamespaceAware 203 | * enable or disable namespace awareness in the underlying 204 | * {@link DocumentBuilderFactory} 205 | * @return 206 | * an XML Document. 207 | * 208 | * @throws FactoryConfigurationError xyz 209 | * @throws ParserConfigurationException xyz 210 | */ 211 | protected static Document createDocumentImpl( 212 | String name, String namespaceURI, boolean enableExternalEntities, 213 | boolean isNamespaceAware) 214 | throws ParserConfigurationException, FactoryConfigurationError 215 | { 216 | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 217 | factory.setNamespaceAware(isNamespaceAware); 218 | enableOrDisableExternalEntityParsing(factory, enableExternalEntities); 219 | DocumentBuilder builder = factory.newDocumentBuilder(); 220 | Document document = builder.newDocument(); 221 | Element rootElement = null; 222 | if (namespaceURI != null && namespaceURI.length() > 0) { 223 | rootElement = document.createElementNS(namespaceURI, name); 224 | } else { 225 | rootElement = document.createElement(name); 226 | } 227 | document.appendChild(rootElement); 228 | return document; 229 | } 230 | 231 | /** 232 | * Return an XML Document parsed from the given input source. 233 | * 234 | * @param inputSource 235 | * an XML document input source that will be parsed into a DOM. 236 | * @param enableExternalEntities 237 | * enable external entities; beware of XML External Entity (XXE) injection. 238 | * @param isNamespaceAware 239 | * enable or disable namespace awareness in the underlying 240 | * {@link DocumentBuilderFactory} 241 | * @return 242 | * a builder node that can be used to add more nodes to the XML document. 243 | * 244 | * @throws ParserConfigurationException xyz 245 | * @throws FactoryConfigurationError xyz 246 | * @throws ParserConfigurationException xyz 247 | * @throws IOException xyz 248 | * @throws SAXException xyz 249 | */ 250 | protected static Document parseDocumentImpl( 251 | InputSource inputSource, boolean enableExternalEntities, 252 | boolean isNamespaceAware) 253 | throws ParserConfigurationException, SAXException, IOException 254 | { 255 | DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 256 | factory.setNamespaceAware(isNamespaceAware); 257 | enableOrDisableExternalEntityParsing(factory, enableExternalEntities); 258 | DocumentBuilder builder = factory.newDocumentBuilder(); 259 | Document document = builder.parse(inputSource); 260 | return document; 261 | } 262 | 263 | /** 264 | * Find and delete from the underlying Document any text nodes that 265 | * contain nothing but whitespace, such as newlines and tab or space 266 | * characters used to indent or pretty-print an XML document. 267 | * 268 | * Uses approach I documented on StackOverflow: 269 | * http://stackoverflow.com/a/979606/4970 270 | * 271 | * @throws XPathExpressionException xyz 272 | */ 273 | protected void stripWhitespaceOnlyTextNodesImpl() 274 | throws XPathExpressionException 275 | { 276 | XPathFactory xpathFactory = XPathFactory.newInstance(); 277 | // XPath to find empty text nodes. 278 | XPathExpression xpathExp = xpathFactory.newXPath().compile( 279 | "//text()[normalize-space(.) = '']"); 280 | NodeList emptyTextNodes = (NodeList) xpathExp.evaluate( 281 | this.getDocument(), XPathConstants.NODESET); 282 | 283 | // Remove each empty text node from document. 284 | for (int i = 0; i < emptyTextNodes.getLength(); i++) { 285 | Node emptyTextNode = emptyTextNodes.item(i); 286 | emptyTextNode.getParentNode().removeChild(emptyTextNode); 287 | } 288 | } 289 | 290 | /** 291 | * Find and delete from the underlying Document any text nodes that 292 | * contain nothing but whitespace, such as newlines and tab or space 293 | * characters used to indent or pretty-print an XML document. 294 | * 295 | * Uses approach I documented on StackOverflow: 296 | * http://stackoverflow.com/a/979606/4970 297 | * 298 | * @return 299 | * a builder node at the same location as before the operation. 300 | * @throws XPathExpressionException xyz 301 | */ 302 | public abstract BaseXMLBuilder stripWhitespaceOnlyTextNodes() 303 | throws XPathExpressionException; 304 | 305 | /** 306 | * Imports another BaseXMLBuilder document into this document at the 307 | * current position. The entire document provided is imported. 308 | * 309 | * @param builder 310 | * the BaseXMLBuilder document to be imported. 311 | */ 312 | protected void importXMLBuilderImpl(BaseXMLBuilder builder) { 313 | assertElementContainsNoOrWhitespaceOnlyTextNodes(this.xmlNode); 314 | Node importedNode = getDocument().importNode( 315 | builder.getDocument().getDocumentElement(), true); 316 | this.xmlNode.appendChild(importedNode); 317 | } 318 | 319 | /** 320 | * @return 321 | * true if the XML Document and Element objects wrapped by this 322 | * builder are equal to the other's wrapped objects. 323 | */ 324 | @Override 325 | public boolean equals(Object obj) { 326 | if (obj != null && obj instanceof BaseXMLBuilder) { 327 | BaseXMLBuilder other = (BaseXMLBuilder) obj; 328 | return 329 | this.xmlDocument.equals(other.getDocument()) 330 | && this.xmlNode.equals(other.getElement()); 331 | } 332 | return false; 333 | } 334 | 335 | /** 336 | * @return 337 | * the XML element wrapped by this builder node, or null if the builder node wraps the 338 | * root Document node. 339 | */ 340 | public Element getElement() { 341 | if (this.xmlNode instanceof Element) { 342 | return (Element) this.xmlNode; 343 | } else { 344 | return null; 345 | } 346 | } 347 | 348 | /** 349 | * @return 350 | * the XML document constructed by all builder nodes. 351 | */ 352 | public Document getDocument() { 353 | return this.xmlDocument; 354 | } 355 | 356 | /** 357 | * Imports another XMLBuilder document into this document at the 358 | * current position. The entire document provided is imported. 359 | * 360 | * @param builder 361 | * the XMLBuilder document to be imported. 362 | * 363 | * @return 364 | * a builder node at the same location as before the import, but 365 | * now containing the entire document tree provided. 366 | */ 367 | public abstract BaseXMLBuilder importXMLBuilder(BaseXMLBuilder builder); 368 | 369 | /** 370 | * @return 371 | * the builder node representing the root element of the XML document. 372 | */ 373 | public abstract BaseXMLBuilder root(); 374 | 375 | /** 376 | * Return the result of evaluating an XPath query on the builder's DOM 377 | * using the given namespace. Returns null if the query finds nothing, 378 | * or finds a node that does not match the type specified by returnType. 379 | * 380 | * @param xpath 381 | * an XPath expression 382 | * @param type 383 | * the type the XPath is expected to resolve to, e.g: 384 | * {@link XPathConstants#NODE}, {@link XPathConstants#NODESET}, 385 | * {@link XPathConstants#STRING}. 386 | * @param nsContext 387 | * a mapping of prefixes to namespace URIs that allows the XPath expression 388 | * to use namespaces, or null for a non-namespaced document. 389 | * 390 | * @return 391 | * a builder node representing the first Element that matches the 392 | * XPath expression. 393 | * 394 | * @throws XPathExpressionException 395 | * If the XPath is invalid, or if does not resolve to at least one 396 | * {@link Node#ELEMENT_NODE}. 397 | */ 398 | public Object xpathQuery(String xpath, QName type, NamespaceContext nsContext) 399 | throws XPathExpressionException { 400 | XPathFactory xpathFactory = XPathFactory.newInstance(); 401 | XPath xPath = xpathFactory.newXPath(); 402 | if (nsContext != null) { 403 | xPath.setNamespaceContext(nsContext); 404 | } 405 | XPathExpression xpathExp = xPath.compile(xpath); 406 | try { 407 | return xpathExp.evaluate(this.xmlNode, type); 408 | } catch (IllegalArgumentException e) { 409 | // Thrown if item found does not match expected type 410 | return null; 411 | } 412 | } 413 | 414 | /** 415 | * Return the result of evaluating an XPath query on the builder's DOM. 416 | * Returns null if the query finds nothing, 417 | * or finds a node that does not match the type specified by returnType. 418 | * 419 | * @param xpath 420 | * an XPath expression 421 | * @param type 422 | * the type the XPath is expected to resolve to, e.g: 423 | * {@link XPathConstants#NODE}, {@link XPathConstants#NODESET}, 424 | * {@link XPathConstants#STRING} 425 | * 426 | * @return 427 | * a builder node representing the first Element that matches the 428 | * XPath expression. 429 | * 430 | * @throws XPathExpressionException 431 | * If the XPath is invalid, or if does not resolve to at least one 432 | * {@link Node#ELEMENT_NODE}. 433 | */ 434 | public Object xpathQuery(String xpath, QName type) 435 | throws XPathExpressionException { 436 | return xpathQuery(xpath, type, null); 437 | } 438 | 439 | /** 440 | * Find the first element in the builder's DOM matching the given 441 | * XPath expression, where the expression may include namespaces if 442 | * a {@link NamespaceContext} is provided. 443 | * 444 | * @param xpath 445 | * An XPath expression that *must* resolve to an existing Element within 446 | * the document object model. 447 | * @param nsContext 448 | * a mapping of prefixes to namespace URIs that allows the XPath expression 449 | * to use namespaces. 450 | * 451 | * @return 452 | * a builder node representing the first Element that matches the 453 | * XPath expression. 454 | * 455 | * @throws XPathExpressionException 456 | * If the XPath is invalid, or if does not resolve to at least one 457 | * {@link Node#ELEMENT_NODE}. 458 | */ 459 | public abstract BaseXMLBuilder xpathFind(String xpath, NamespaceContext nsContext) 460 | throws XPathExpressionException; 461 | 462 | /** 463 | * Find the first element in the builder's DOM matching the given 464 | * XPath expression. 465 | * 466 | * @param xpath 467 | * An XPath expression that *must* resolve to an existing Element within 468 | * the document object model. 469 | * 470 | * @return 471 | * a builder node representing the first Element that matches the 472 | * XPath expression. 473 | * 474 | * @throws XPathExpressionException 475 | * If the XPath is invalid, or if does not resolve to at least one 476 | * {@link Node#ELEMENT_NODE}. 477 | */ 478 | public abstract BaseXMLBuilder xpathFind(String xpath) 479 | throws XPathExpressionException; 480 | 481 | /** 482 | * Find the first element in the builder's DOM matching the given 483 | * XPath expression, where the expression may include namespaces if 484 | * a {@link NamespaceContext} is provided. 485 | * 486 | * @param xpath 487 | * An XPath expression that *must* resolve to an existing Element within 488 | * the document object model. 489 | * @param nsContext 490 | * a mapping of prefixes to namespace URIs that allows the XPath expression 491 | * to use namespaces. 492 | * 493 | * @return 494 | * the first Element that matches the XPath expression. 495 | * 496 | * @throws XPathExpressionException 497 | * If the XPath is invalid, or if does not resolve to at least one 498 | * {@link Node#ELEMENT_NODE}. 499 | */ 500 | protected Node xpathFindImpl(String xpath, NamespaceContext nsContext) 501 | throws XPathExpressionException 502 | { 503 | Node foundNode = (Node) this.xpathQuery(xpath, XPathConstants.NODE, nsContext); 504 | if (foundNode == null || foundNode.getNodeType() != Node.ELEMENT_NODE) { 505 | throw new XPathExpressionException("XPath expression \"" 506 | + xpath + "\" does not resolve to an Element in context " 507 | + this.xmlNode + ": " + foundNode); 508 | } 509 | return foundNode; 510 | } 511 | 512 | /** 513 | * Look up the namespace matching the current builder node's qualified 514 | * name prefix (if any) or the document's default namespace. 515 | * 516 | * @param name 517 | * the name of the XML element. 518 | * 519 | * @return 520 | * The namespace URI, or null if none applies. 521 | */ 522 | protected String lookupNamespaceURIImpl(String name) { 523 | String prefix = getPrefixFromQualifiedName(name); 524 | String namespaceURI = this.xmlNode.lookupNamespaceURI(prefix); 525 | return namespaceURI; 526 | } 527 | 528 | /** 529 | * Add a named and namespaced XML element to the document as a child of 530 | * this builder's node. 531 | * 532 | * @param name 533 | * the name of the XML element. 534 | * @param namespaceURI 535 | * a namespace URI 536 | * @return xyz 537 | * 538 | * @throws IllegalStateException 539 | * if you attempt to add a child element to an XML node that already 540 | * contains a text node value. 541 | */ 542 | protected Element elementImpl(String name, String namespaceURI) { 543 | assertElementContainsNoOrWhitespaceOnlyTextNodes(this.xmlNode); 544 | if (namespaceURI == null) { 545 | return getDocument().createElement(name); 546 | } else { 547 | return getDocument().createElementNS(namespaceURI, name); 548 | } 549 | } 550 | 551 | /** 552 | * Add a named XML element to the document as a child of this builder node, 553 | * and return the builder node representing the new child. 554 | * 555 | * When adding an element to a namespaced document, the new node will be 556 | * assigned a namespace matching it's qualified name prefix (if any) or 557 | * the document's default namespace. NOTE: If the element has a prefix that 558 | * does not match any known namespaces, the element will be created 559 | * without any namespace. 560 | * 561 | * @param name 562 | * the name of the XML element. 563 | * 564 | * @return 565 | * a builder node representing the new child. 566 | * 567 | * @throws IllegalStateException 568 | * if you attempt to add a child element to an XML node that already 569 | * contains a text node value. 570 | */ 571 | public abstract BaseXMLBuilder element(String name); 572 | 573 | /** 574 | * Synonym for {@link #element(String)}. 575 | * 576 | * @param name 577 | * the name of the XML element. 578 | * 579 | * @return 580 | * a builder node representing the new child. 581 | * 582 | * @throws IllegalStateException 583 | * if you attempt to add a child element to an XML node that already 584 | * contains a text node value. 585 | */ 586 | public abstract BaseXMLBuilder elem(String name); 587 | 588 | /** 589 | * Synonym for {@link #element(String)}. 590 | * 591 | * @param name 592 | * the name of the XML element. 593 | * 594 | * @return 595 | * a builder node representing the new child. 596 | * 597 | * @throws IllegalStateException 598 | * if you attempt to add a child element to an XML node that already 599 | * contains a text node value. 600 | */ 601 | public abstract BaseXMLBuilder e(String name); 602 | 603 | /** 604 | * Add a named and namespaced XML element to the document as a child of 605 | * this builder node, and return the builder node representing the new child. 606 | * 607 | * @param name 608 | * the name of the XML element. 609 | * @param namespaceURI 610 | * a namespace URI 611 | * 612 | * @return 613 | * a builder node representing the new child. 614 | * 615 | * @throws IllegalStateException 616 | * if you attempt to add a child element to an XML node that already 617 | * contains a text node value. 618 | */ 619 | public abstract BaseXMLBuilder element(String name, String namespaceURI); 620 | 621 | /** 622 | * Add a named XML element to the document as a sibling element 623 | * that precedes the position of this builder node, and return the builder node 624 | * representing the new child. 625 | * 626 | * When adding an element to a namespaced document, the new node will be 627 | * assigned a namespace matching it's qualified name prefix (if any) or 628 | * the document's default namespace. NOTE: If the element has a prefix that 629 | * does not match any known namespaces, the element will be created 630 | * without any namespace. 631 | * 632 | * @param name 633 | * the name of the XML element. 634 | * 635 | * @return 636 | * a builder node representing the new child. 637 | * 638 | * @throws IllegalStateException 639 | * if you attempt to add a sibling element to a node where there are already 640 | * one or more siblings that are text nodes. 641 | */ 642 | public abstract BaseXMLBuilder elementBefore(String name); 643 | 644 | /** 645 | * Add a named and namespaced XML element to the document as a sibling element 646 | * that precedes the position of this builder node, and return the builder node 647 | * representing the new child. 648 | * 649 | * @param name 650 | * the name of the XML element. 651 | * @param namespaceURI 652 | * a namespace URI 653 | * 654 | * @return 655 | * a builder node representing the new child. 656 | * 657 | * @throws IllegalStateException 658 | * if you attempt to add a sibling element to a node where there are already 659 | * one or more siblings that are text nodes. 660 | */ 661 | public abstract BaseXMLBuilder elementBefore(String name, String namespaceURI); 662 | 663 | /** 664 | * Add a named attribute value to the element represented by this builder 665 | * node, and return the node representing the element to which the 666 | * attribute was added (not the new attribute node). 667 | * 668 | * @param name 669 | * the attribute's name. 670 | * @param value 671 | * the attribute's value. 672 | * 673 | * @return 674 | * the builder node representing the element to which the attribute was 675 | * added. 676 | */ 677 | public abstract BaseXMLBuilder attribute(String name, String value); 678 | 679 | /** 680 | * Synonym for {@link #attribute(String, String)}. 681 | * 682 | * @param name 683 | * the attribute's name. 684 | * @param value 685 | * the attribute's value. 686 | * 687 | * @return 688 | * the builder node representing the element to which the attribute was 689 | * added. 690 | */ 691 | public abstract BaseXMLBuilder attr(String name, String value); 692 | 693 | /** 694 | * Synonym for {@link #attribute(String, String)}. 695 | * 696 | * @param name 697 | * the attribute's name. 698 | * @param value 699 | * the attribute's value. 700 | * 701 | * @return 702 | * the builder node representing the element to which the attribute was 703 | * added. 704 | */ 705 | public abstract BaseXMLBuilder a(String name, String value); 706 | 707 | 708 | /** 709 | * Add or replace the text value of an element represented by this builder 710 | * node, and return the node representing the element to which the text 711 | * was added (not the new text node). 712 | * 713 | * @param value 714 | * the text value to set or add to the element. 715 | * @param replaceText 716 | * if True any existing text content of the node is replaced with the 717 | * given text value, if the given value is appended to any existing text. 718 | * 719 | * @return 720 | * the builder node representing the element to which the text was added. 721 | */ 722 | public abstract BaseXMLBuilder text(String value, boolean replaceText); 723 | 724 | /** 725 | * Add a text value to the element represented by this builder node, and 726 | * return the node representing the element to which the text 727 | * was added (not the new text node). 728 | * 729 | * @param value 730 | * the text value to add to the element. 731 | * 732 | * @return 733 | * the builder node representing the element to which the text was added. 734 | */ 735 | public abstract BaseXMLBuilder text(String value); 736 | 737 | /** 738 | * Synonym for {@link #text(String)}. 739 | * 740 | * @param value 741 | * the text value to add to the element. 742 | * 743 | * @return 744 | * the builder node representing the element to which the text was added. 745 | */ 746 | public abstract BaseXMLBuilder t(String value); 747 | 748 | /** 749 | * Add a named XML element to the document as a sibling element 750 | * that precedes the position of this builder node. 751 | * 752 | * When adding an element to a namespaced document, the new node will be 753 | * assigned a namespace matching it's qualified name prefix (if any) or 754 | * the document's default namespace. NOTE: If the element has a prefix that 755 | * does not match any known namespaces, the element will be created 756 | * without any namespace. 757 | * 758 | * @param name 759 | * the name of the XML element. 760 | * @return xyz 761 | * 762 | * @throws IllegalStateException 763 | * if you attempt to add a sibling element to a node where there are already 764 | * one or more siblings that are text nodes. 765 | */ 766 | protected Element elementBeforeImpl(String name) { 767 | String prefix = getPrefixFromQualifiedName(name); 768 | String namespaceURI = this.xmlNode.lookupNamespaceURI(prefix); 769 | return elementBeforeImpl(name, namespaceURI); 770 | } 771 | 772 | /** 773 | * Add a named and namespaced XML element to the document as a sibling element 774 | * that precedes the position of this builder node. 775 | * 776 | * @param name 777 | * the name of the XML element. 778 | * @param namespaceURI 779 | * a namespace URI 780 | * @return xyz 781 | * 782 | * @throws IllegalStateException 783 | * if you attempt to add a sibling element to a node where there are already 784 | * one or more siblings that are text nodes. 785 | */ 786 | protected Element elementBeforeImpl(String name, String namespaceURI) { 787 | Node parentNode = this.xmlNode.getParentNode(); 788 | assertElementContainsNoOrWhitespaceOnlyTextNodes(parentNode); 789 | 790 | Element newElement = (namespaceURI == null 791 | ? getDocument().createElement(name) 792 | : getDocument().createElementNS(namespaceURI, name)); 793 | 794 | // Insert new element before the current element 795 | parentNode.insertBefore(newElement, this.xmlNode); 796 | // Return a new builder node pointing at the new element 797 | return newElement; 798 | } 799 | 800 | /** 801 | * Add a named attribute value to the element for this builder node. 802 | * 803 | * @param name 804 | * the attribute's name. 805 | * @param value 806 | * the attribute's value. 807 | */ 808 | protected void attributeImpl(String name, String value) { 809 | if (! (this.xmlNode instanceof Element)) { 810 | throw new RuntimeException( 811 | "Cannot add an attribute to non-Element underlying node: " 812 | + this.xmlNode); 813 | } 814 | ((Element) xmlNode).setAttribute(name, value); 815 | } 816 | 817 | /** 818 | * Add or replace the text value of an element for this builder node. 819 | * 820 | * @param value 821 | * the text value to set or add to the element. 822 | * @param replaceText 823 | * if True any existing text content of the node is replaced with the 824 | * given text value, if the given value is appended to any existing text. 825 | */ 826 | protected void textImpl(String value, boolean replaceText) { 827 | // Issue 10: null text values cause exceptions on subsequent call to 828 | // Transformer to render document, so we fail-fast here on bad data. 829 | if (value == null) { 830 | throw new IllegalArgumentException("Illegal null text value"); 831 | } 832 | 833 | if (replaceText) { 834 | xmlNode.setTextContent(value); 835 | } else { 836 | xmlNode.appendChild(getDocument().createTextNode(value)); 837 | } 838 | } 839 | 840 | 841 | /** 842 | * Add a CDATA node with String content to the element for this builder node. 843 | * 844 | * @param data 845 | * the String value that will be added to a CDATA element. 846 | */ 847 | protected void cdataImpl(String data) { 848 | xmlNode.appendChild( 849 | getDocument().createCDATASection(data)); 850 | } 851 | 852 | /** 853 | * Add a CDATA node with Base64-encoded byte data content to the element 854 | * for this builder node. 855 | * 856 | * @param data 857 | * the data value that will be Base64-encoded and added to a CDATA element. 858 | */ 859 | protected void cdataImpl(byte[] data) { 860 | xmlNode.appendChild( 861 | getDocument().createCDATASection( 862 | Base64.encodeBytes(data))); 863 | } 864 | 865 | /** 866 | * Add a comment to the element represented by this builder node. 867 | * 868 | * @param comment 869 | * the comment to add to the element. 870 | */ 871 | protected void commentImpl(String comment) { 872 | xmlNode.appendChild(getDocument().createComment(comment)); 873 | } 874 | 875 | /** 876 | * Add an instruction to the element represented by this builder node. 877 | * 878 | * @param target 879 | * the target value for the instruction. 880 | * @param data 881 | * the data value for the instruction 882 | */ 883 | protected void instructionImpl(String target, String data) { 884 | xmlNode.appendChild(getDocument().createProcessingInstruction(target, data)); 885 | } 886 | 887 | /** 888 | * Insert an instruction before the element represented by this builder node. 889 | * 890 | * @param target 891 | * the target value for the instruction. 892 | * @param data 893 | * the data value for the instruction 894 | */ 895 | protected void insertInstructionImpl(String target, String data) { 896 | getDocument().insertBefore( 897 | getDocument().createProcessingInstruction(target, data), 898 | xmlNode); 899 | } 900 | 901 | /** 902 | * Add a reference to the element represented by this builder node. 903 | * 904 | * @param name 905 | * the name value for the reference. 906 | */ 907 | protected void referenceImpl(String name) { 908 | xmlNode.appendChild(getDocument().createEntityReference(name)); 909 | } 910 | 911 | /** 912 | * Add an XML namespace attribute to this builder's element node. 913 | * 914 | * @param prefix 915 | * a prefix for the namespace URI within the document, may be null 916 | * or empty in which case a default "xmlns" attribute is created. 917 | * @param namespaceURI 918 | * a namespace uri 919 | */ 920 | protected void namespaceImpl(String prefix, String namespaceURI) { 921 | if (! (this.xmlNode instanceof Element)) { 922 | throw new RuntimeException( 923 | "Cannot add an attribute to non-Element underlying node: " 924 | + this.xmlNode); 925 | } 926 | if (prefix != null && prefix.length() > 0) { 927 | ((Element) xmlNode).setAttributeNS("http://www.w3.org/2000/xmlns/", 928 | "xmlns:" + prefix, namespaceURI); 929 | } else { 930 | ((Element) xmlNode).setAttributeNS("http://www.w3.org/2000/xmlns/", 931 | "xmlns", namespaceURI); 932 | } 933 | } 934 | 935 | /** 936 | * Add an XML namespace attribute to this builder's element node 937 | * without a prefix. 938 | * 939 | * @param namespaceURI 940 | * a namespace uri 941 | */ 942 | protected void namespaceImpl(String namespaceURI) { 943 | namespaceImpl(null, namespaceURI); 944 | } 945 | 946 | /** 947 | * Add a CDATA node with String content to the element represented by this 948 | * builder node, and return the node representing the element to which the 949 | * data was added (not the new CDATA node). 950 | * 951 | * @param data 952 | * the String value that will be added to a CDATA element. 953 | * 954 | * @return 955 | * the builder node representing the element to which the data was added. 956 | */ 957 | public abstract BaseXMLBuilder cdata(String data); 958 | 959 | /** 960 | * Synonym for {@link #cdata(String)}. 961 | * 962 | * @param data 963 | * the String value that will be added to a CDATA element. 964 | * 965 | * @return 966 | * the builder node representing the element to which the data was added. 967 | */ 968 | public abstract BaseXMLBuilder data(String data); 969 | 970 | /** 971 | * Synonym for {@link #cdata(String)}. 972 | * 973 | * @param data 974 | * the String value that will be added to a CDATA element. 975 | * 976 | * @return 977 | * the builder node representing the element to which the data was added. 978 | */ 979 | public abstract BaseXMLBuilder d(String data); 980 | 981 | /** 982 | * Add a CDATA node with Base64-encoded byte data content to the element represented 983 | * by this builder node, and return the node representing the element to which the 984 | * data was added (not the new CDATA node). 985 | * 986 | * @param data 987 | * the data value that will be Base64-encoded and added to a CDATA element. 988 | * 989 | * @return 990 | * the builder node representing the element to which the data was added. 991 | */ 992 | public abstract BaseXMLBuilder cdata(byte[] data); 993 | 994 | /** 995 | * Synonym for {@link #cdata(byte[])}. 996 | * 997 | * @param data 998 | * the data value that will be Base64-encoded and added to a CDATA element. 999 | * 1000 | * @return 1001 | * the builder node representing the element to which the data was added. 1002 | */ 1003 | public abstract BaseXMLBuilder data(byte[] data); 1004 | 1005 | /** 1006 | * Synonym for {@link #cdata(byte[])}. 1007 | * 1008 | * @param data 1009 | * the data value that will be Base64-encoded and added to a CDATA element. 1010 | * 1011 | * @return 1012 | * the builder node representing the element to which the data was added. 1013 | */ 1014 | public abstract BaseXMLBuilder d(byte[] data); 1015 | 1016 | /** 1017 | * Add a comment to the element represented by this builder node, and 1018 | * return the node representing the element to which the comment 1019 | * was added (not the new comment node). 1020 | * 1021 | * @param comment 1022 | * the comment to add to the element. 1023 | * 1024 | * @return 1025 | * the builder node representing the element to which the comment was added. 1026 | */ 1027 | public abstract BaseXMLBuilder comment(String comment); 1028 | 1029 | /** 1030 | * Synonym for {@link #comment(String)}. 1031 | * 1032 | * @param comment 1033 | * the comment to add to the element. 1034 | * 1035 | * @return 1036 | * the builder node representing the element to which the comment was added. 1037 | */ 1038 | public abstract BaseXMLBuilder cmnt(String comment); 1039 | 1040 | /** 1041 | * Synonym for {@link #comment(String)}. 1042 | * 1043 | * @param comment 1044 | * the comment to add to the element. 1045 | * 1046 | * @return 1047 | * the builder node representing the element to which the comment was added. 1048 | */ 1049 | public abstract BaseXMLBuilder c(String comment); 1050 | 1051 | /** 1052 | * Add an instruction to the element represented by this builder node, and 1053 | * return the node representing the element to which the instruction 1054 | * was added (not the new instruction node). 1055 | * 1056 | * @param target 1057 | * the target value for the instruction. 1058 | * @param data 1059 | * the data value for the instruction 1060 | * 1061 | * @return 1062 | * the builder node representing the element to which the instruction was 1063 | * added. 1064 | */ 1065 | public abstract BaseXMLBuilder instruction(String target, String data); 1066 | 1067 | /** 1068 | * Synonym for {@link #instruction(String, String)}. 1069 | * 1070 | * @param target 1071 | * the target value for the instruction. 1072 | * @param data 1073 | * the data value for the instruction 1074 | * 1075 | * @return 1076 | * the builder node representing the element to which the instruction was 1077 | * added. 1078 | */ 1079 | public abstract BaseXMLBuilder inst(String target, String data); 1080 | 1081 | /** 1082 | * Synonym for {@link #instruction(String, String)}. 1083 | * 1084 | * @param target 1085 | * the target value for the instruction. 1086 | * @param data 1087 | * the data value for the instruction 1088 | * 1089 | * @return 1090 | * the builder node representing the element to which the instruction was 1091 | * added. 1092 | */ 1093 | public abstract BaseXMLBuilder i(String target, String data); 1094 | 1095 | /** 1096 | * Insert an instruction before the element represented by this builder node, 1097 | * and return the node representing that same element 1098 | * (not the new instruction node). 1099 | * 1100 | * @param target 1101 | * the target value for the instruction. 1102 | * @param data 1103 | * the data value for the instruction 1104 | * 1105 | * @return 1106 | * the builder node representing the element before which the instruction was inserted. 1107 | */ 1108 | public abstract BaseXMLBuilder insertInstruction(String target, String data); 1109 | 1110 | /** 1111 | * Add a reference to the element represented by this builder node, and 1112 | * return the node representing the element to which the reference 1113 | * was added (not the new reference node). 1114 | * 1115 | * @param name 1116 | * the name value for the reference. 1117 | * 1118 | * @return 1119 | * the builder node representing the element to which the reference was 1120 | * added. 1121 | */ 1122 | public abstract BaseXMLBuilder reference(String name); 1123 | 1124 | /** 1125 | * Synonym for {@link #reference(String)}. 1126 | * 1127 | * @param name 1128 | * the name value for the reference. 1129 | * 1130 | * @return 1131 | * the builder node representing the element to which the reference was 1132 | * added. 1133 | */ 1134 | public abstract BaseXMLBuilder ref(String name); 1135 | 1136 | /** 1137 | * Synonym for {@link #reference(String)}. 1138 | * 1139 | * @param name 1140 | * the name value for the reference. 1141 | * 1142 | * @return 1143 | * the builder node representing the element to which the reference was 1144 | * added. 1145 | */ 1146 | public abstract BaseXMLBuilder r(String name); 1147 | 1148 | /** 1149 | * Add an XML namespace attribute to this builder's element node. 1150 | * 1151 | * @param prefix 1152 | * a prefix for the namespace URI within the document, may be null 1153 | * or empty in which case a default "xmlns" attribute is created. 1154 | * @param namespaceURI 1155 | * a namespace uri 1156 | * 1157 | * @return 1158 | * the builder node representing the element to which the attribute was added. 1159 | */ 1160 | public abstract BaseXMLBuilder namespace(String prefix, String namespaceURI); 1161 | 1162 | /** 1163 | * Synonym for {@link #namespace(String, String)}. 1164 | * 1165 | * @param prefix 1166 | * a prefix for the namespace URI within the document, may be null 1167 | * or empty in which case a default xmlns attribute is created. 1168 | * @param namespaceURI 1169 | * a namespace uri 1170 | * 1171 | * @return 1172 | * the builder node representing the element to which the attribute was added. 1173 | */ 1174 | public abstract BaseXMLBuilder ns(String prefix, String namespaceURI); 1175 | 1176 | /** 1177 | * Add an XML namespace attribute to this builder's element node 1178 | * without a prefix. 1179 | * 1180 | * @param namespaceURI 1181 | * a namespace uri 1182 | * 1183 | * @return 1184 | * the builder node representing the element to which the attribute was added. 1185 | */ 1186 | public abstract BaseXMLBuilder namespace(String namespaceURI); 1187 | 1188 | /** 1189 | * Synonym for {@link #namespace(String)}. 1190 | * 1191 | * @param namespaceURI 1192 | * a namespace uri 1193 | * 1194 | * @return 1195 | * the builder node representing the element to which the attribute was added. 1196 | */ 1197 | public abstract BaseXMLBuilder ns(String namespaceURI); 1198 | 1199 | 1200 | /** 1201 | * Return the builder node representing the nth ancestor element 1202 | * of this node, or the root node if n exceeds the document's depth. 1203 | * 1204 | * @param steps 1205 | * the number of parent elements to step over while navigating up the chain 1206 | * of node ancestors. A steps value of 1 will find a node's parent, 2 will 1207 | * find its grandparent etc. 1208 | * 1209 | * @return 1210 | * the nth ancestor of this node, or the root node if this is 1211 | * reached before the nth parent is found. 1212 | */ 1213 | public abstract BaseXMLBuilder up(int steps); 1214 | 1215 | /** 1216 | * Return the builder node representing the parent of the current node. 1217 | * 1218 | * @return 1219 | * the parent of this node, or the root node if this method is called on the 1220 | * root node. 1221 | */ 1222 | public abstract BaseXMLBuilder up(); 1223 | 1224 | /** 1225 | * BEWARE: The builder returned by this method represents a Document node, not 1226 | * an Element node as is usually the case, so attempts to use the attribute or 1227 | * namespace methods on this builder will likely fail. 1228 | * 1229 | * @return 1230 | * the builder node representing the root XML document. 1231 | */ 1232 | public abstract BaseXMLBuilder document(); 1233 | 1234 | /** 1235 | * Return the Document node representing the nth ancestor element 1236 | * of this node, or the root node if n exceeds the document's depth. 1237 | * 1238 | * @param steps 1239 | * the number of parent elements to step over while navigating up the chain 1240 | * of node ancestors. A steps value of 1 will find a node's parent, 2 will 1241 | * find its grandparent etc. 1242 | * 1243 | * @return 1244 | * the nth ancestor of this node, or the root node if this is 1245 | * reached before the nth parent is found. 1246 | */ 1247 | protected Node upImpl(int steps) { 1248 | Node currNode = this.xmlNode; 1249 | int stepCount = 0; 1250 | while (currNode.getParentNode() != null && stepCount < steps) { 1251 | currNode = currNode.getParentNode(); 1252 | stepCount++; 1253 | } 1254 | return currNode; 1255 | } 1256 | 1257 | /** 1258 | * @param anXmlElement xyz 1259 | * 1260 | * @throws IllegalStateException 1261 | * if the current element contains any child text nodes that aren't pure whitespace. 1262 | * We allow whitespace so parsed XML documents containing indenting or pretty-printing 1263 | * can still be amended, per issue #17. 1264 | */ 1265 | protected void assertElementContainsNoOrWhitespaceOnlyTextNodes(Node anXmlElement) 1266 | { 1267 | Node textNodeWithNonWhitespace = null; 1268 | NodeList childNodes = anXmlElement.getChildNodes(); 1269 | for (int i = 0; i < childNodes.getLength(); i++) { 1270 | if (Element.TEXT_NODE == childNodes.item(i).getNodeType()) { 1271 | Node textNode = childNodes.item(i); 1272 | String textWithoutWhitespace = 1273 | textNode.getTextContent().replaceAll("\\s", ""); 1274 | if (textWithoutWhitespace.length() > 0) { 1275 | textNodeWithNonWhitespace = textNode; 1276 | break; 1277 | } 1278 | } 1279 | } 1280 | if (textNodeWithNonWhitespace != null) { 1281 | throw new IllegalStateException( 1282 | "Cannot add sub-element to element <" + anXmlElement.getNodeName() 1283 | + "> that contains a Text node that isn't purely whitespace: " 1284 | + textNodeWithNonWhitespace); 1285 | } 1286 | } 1287 | 1288 | /** 1289 | * Serialize either the specific Element wrapped by this BaseXMLBuilder, 1290 | * or its entire XML document, to the given writer using the default 1291 | * {@link TransformerFactory} and {@link Transformer} classes. 1292 | * If output options are provided, these options are provided to the 1293 | * {@link Transformer} serializer. 1294 | * 1295 | * @param wholeDocument 1296 | * if true the whole XML document (i.e. the document root) is serialized, 1297 | * if false just the current Element and its descendants are serialized. 1298 | * @param writer 1299 | * a writer to which the serialized document is written. 1300 | * @param outputProperties 1301 | * settings for the {@link Transformer} serializer. This parameter may be 1302 | * null or an empty Properties object, in which case the default output 1303 | * properties will be applied. 1304 | * 1305 | * @throws TransformerException xyz 1306 | */ 1307 | public void toWriter(boolean wholeDocument, Writer writer, Properties outputProperties) 1308 | throws TransformerException { 1309 | StreamResult streamResult = new StreamResult(writer); 1310 | 1311 | DOMSource domSource = null; 1312 | if (wholeDocument) { 1313 | domSource = new DOMSource(getDocument()); 1314 | } else { 1315 | domSource = new DOMSource(getElement()); 1316 | } 1317 | 1318 | TransformerFactory tf = TransformerFactory.newInstance(); 1319 | Transformer serializer = tf.newTransformer(); 1320 | 1321 | if (outputProperties != null) { 1322 | for (Entry entry: outputProperties.entrySet()) { 1323 | serializer.setOutputProperty( 1324 | (String) entry.getKey(), 1325 | (String) entry.getValue()); 1326 | } 1327 | } 1328 | serializer.transform(domSource, streamResult); 1329 | } 1330 | 1331 | /** 1332 | * Serialize the XML document to the given writer using the default 1333 | * {@link TransformerFactory} and {@link Transformer} classes. If output 1334 | * options are provided, these options are provided to the 1335 | * {@link Transformer} serializer. 1336 | * 1337 | * @param writer 1338 | * a writer to which the serialized document is written. 1339 | * @param outputProperties 1340 | * settings for the {@link Transformer} serializer. This parameter may be 1341 | * null or an empty Properties object, in which case the default output 1342 | * properties will be applied. 1343 | * 1344 | * @throws TransformerException xyz 1345 | */ 1346 | public void toWriter(Writer writer, Properties outputProperties) 1347 | throws TransformerException { 1348 | this.toWriter(true, writer, outputProperties); 1349 | } 1350 | 1351 | /** 1352 | * Serialize the XML document to a string by delegating to the 1353 | * {@link #toWriter(Writer, Properties)} method. If output options are 1354 | * provided, these options are provided to the {@link Transformer} 1355 | * serializer. 1356 | * 1357 | * @param outputProperties 1358 | * settings for the {@link Transformer} serializer. This parameter may be 1359 | * null or an empty Properties object, in which case the default output 1360 | * properties will be applied. 1361 | * 1362 | * @return 1363 | * the XML document as a string 1364 | * 1365 | * @throws TransformerException xyz 1366 | */ 1367 | public String asString(Properties outputProperties) throws TransformerException { 1368 | StringWriter writer = new StringWriter(); 1369 | toWriter(writer, outputProperties); 1370 | return writer.toString(); 1371 | } 1372 | 1373 | /** 1374 | * Serialize the current XML Element and its descendants to a string by 1375 | * delegating to the {@link #toWriter(Writer, Properties)} method. 1376 | * If output options are provided, these options are provided to the 1377 | * {@link Transformer} serializer. 1378 | * 1379 | * @param outputProperties 1380 | * settings for the {@link Transformer} serializer. This parameter may be 1381 | * null or an empty Properties object, in which case the default output 1382 | * properties will be applied. 1383 | * 1384 | * @return 1385 | * the XML document as a string 1386 | * 1387 | * @throws TransformerException xyz 1388 | */ 1389 | public String elementAsString(Properties outputProperties) throws TransformerException { 1390 | StringWriter writer = new StringWriter(); 1391 | toWriter(false, writer, outputProperties); 1392 | return writer.toString(); 1393 | } 1394 | 1395 | /** 1396 | * Serialize the XML document to a string excluding the XML declaration. 1397 | * 1398 | * @return 1399 | * the XML document as a string without the XML declaration at the 1400 | * beginning of the output. 1401 | * 1402 | * @throws TransformerException xyz 1403 | */ 1404 | public String asString() throws TransformerException { 1405 | Properties outputProperties = new Properties(); 1406 | outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes"); 1407 | return asString(outputProperties); 1408 | } 1409 | 1410 | /** 1411 | * Serialize the current XML Element and its descendants to a string 1412 | * excluding the XML declaration. 1413 | * 1414 | * @return 1415 | * the XML document as a string without the XML declaration at the 1416 | * beginning of the output. 1417 | * 1418 | * @throws TransformerException xyz 1419 | */ 1420 | public String elementAsString() throws TransformerException { 1421 | Properties outputProperties = new Properties(); 1422 | outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes"); 1423 | return elementAsString(outputProperties); 1424 | } 1425 | 1426 | /** 1427 | * @return 1428 | * a namespace context containing the prefixes and namespace URI's used 1429 | * within this builder's document, to assist in running namespace-aware 1430 | * XPath queries against the document. 1431 | */ 1432 | protected NamespaceContextImpl buildDocumentNamespaceContext() { 1433 | return new NamespaceContextImpl(xmlDocument.getDocumentElement()); 1434 | } 1435 | 1436 | protected String getPrefixFromQualifiedName(String qualifiedName) { 1437 | int colonPos = qualifiedName.indexOf(':'); 1438 | if (colonPos > 0) { 1439 | return qualifiedName.substring(0, colonPos); 1440 | } else { 1441 | return null; 1442 | } 1443 | } 1444 | 1445 | } 1446 | -------------------------------------------------------------------------------- /src/main/java/com/jamesmurty/utils/NamespaceContextImpl.java: -------------------------------------------------------------------------------- 1 | package com.jamesmurty.utils; 2 | 3 | import java.util.Collections; 4 | import java.util.HashMap; 5 | import java.util.HashSet; 6 | import java.util.Iterator; 7 | import java.util.Map; 8 | import java.util.Set; 9 | 10 | import javax.xml.namespace.NamespaceContext; 11 | 12 | import org.w3c.dom.Element; 13 | 14 | /** 15 | * Mappings between prefix strings and namespace URI strings, as required to 16 | * perform XPath queries on namespaced XML documents. 17 | * 18 | * @author jmurty 19 | */ 20 | public class NamespaceContextImpl implements NamespaceContext { 21 | protected Element element = null; 22 | protected Map prefixToNsUriMap = new HashMap(); 23 | protected Map> nsUriToPrefixesMap = new HashMap>(); 24 | 25 | /** 26 | * Create an empty namespace context. 27 | */ 28 | public NamespaceContextImpl() { 29 | } 30 | 31 | /** 32 | * Create a namespace context that will lookup namespace 33 | * information in the given element. 34 | * 35 | * @param element 36 | * Element in which to look up namespace information. 37 | */ 38 | public NamespaceContextImpl(Element element) { 39 | this.element = element; 40 | } 41 | 42 | /** 43 | * Add a custom mapping from prefix to a namespace. This mapping will 44 | * override any mappings present in this class's XML Element (if provided). 45 | * 46 | * @param prefix 47 | * the namespace's prefix. Use an empty string for the 48 | * default prefix. 49 | * @param namespaceURI 50 | * the namespace URI to map. 51 | */ 52 | public void addNamespace(String prefix, String namespaceURI) { 53 | this.prefixToNsUriMap.put(prefix, namespaceURI); 54 | if (this.nsUriToPrefixesMap.get(namespaceURI) == null) { 55 | this.nsUriToPrefixesMap.put(namespaceURI, new HashSet()); 56 | } 57 | this.nsUriToPrefixesMap.get(namespaceURI).add(prefix); 58 | } 59 | 60 | public String getNamespaceURI(String prefix) { 61 | String namespaceURI = this.prefixToNsUriMap.get(prefix); 62 | if (namespaceURI == null && this.element != null) { 63 | // Need null to find default namespace, not an empty string 64 | if (prefix != null && prefix.length() == 0) { 65 | prefix = null; 66 | } 67 | namespaceURI = this.element.lookupNamespaceURI(prefix); 68 | } 69 | return namespaceURI; 70 | } 71 | 72 | public String getPrefix(String namespaceURI) { 73 | Set prefixes = this.nsUriToPrefixesMap.get(namespaceURI); 74 | if (prefixes != null && prefixes.size() > 0) { 75 | return prefixes.iterator().next(); 76 | } 77 | if (this.element != null) { 78 | return this.element.lookupPrefix(namespaceURI); 79 | } 80 | return null; 81 | } 82 | 83 | // Not implemented 84 | @SuppressWarnings({ "rawtypes" }) 85 | public Iterator getPrefixes(String namespaceURI) { 86 | return Collections.EMPTY_LIST.iterator(); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/jamesmurty/utils/XMLBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2008-2020 James Murty (github.com/jmurty) 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 | * This code is available from the GitHub code repository at: 18 | * https://github.com/jmurty/java-xmlbuilder 19 | */ 20 | package com.jamesmurty.utils; 21 | 22 | import java.io.File; 23 | import java.io.FileReader; 24 | import java.io.IOException; 25 | import java.io.StringReader; 26 | 27 | import javax.xml.namespace.NamespaceContext; 28 | import javax.xml.parsers.DocumentBuilder; 29 | import javax.xml.parsers.DocumentBuilderFactory; 30 | import javax.xml.parsers.FactoryConfigurationError; 31 | import javax.xml.parsers.ParserConfigurationException; 32 | import javax.xml.xpath.XPathExpressionException; 33 | 34 | import org.w3c.dom.Document; 35 | import org.w3c.dom.Element; 36 | import org.w3c.dom.Node; 37 | import org.xml.sax.InputSource; 38 | import org.xml.sax.SAXException; 39 | 40 | /** 41 | * XML Builder is a utility that creates simple XML documents using relatively 42 | * sparse Java code. It is intended to allow for quick and painless creation of 43 | * XML documents where you might otherwise be tempted to use concatenated 44 | * strings, rather than face the tedium and verbosity of coding with 45 | * JAXP (http://jaxp.dev.java.net/). 46 | *

47 | * Internally, XML Builder uses JAXP to build a standard W3C 48 | * {@link org.w3c.dom.Document} model (DOM) that you can easily export as a 49 | * string, or access and manipulate further if you have special requirements. 50 | *

51 | *

52 | * The XMLBuilder class serves as a wrapper of {@link org.w3c.dom.Element} nodes, 53 | * and provides a number of utility methods that make it simple to 54 | * manipulate the underlying element and the document to which it belongs. 55 | * In essence, this class performs dual roles: it represents a specific XML 56 | * node, and also allows manipulation of the entire underlying XML document. 57 | * The platform's default {@link DocumentBuilderFactory} and 58 | * {@link DocumentBuilder} classes are used to build the document. 59 | *

60 | * 61 | * @author James Murty 62 | */ 63 | public final class XMLBuilder extends BaseXMLBuilder { 64 | 65 | /** 66 | * Construct a new builder object that wraps the given XML document. 67 | * This constructor is for internal use only. 68 | * 69 | * @param xmlDocument 70 | * an XML document that the builder will manage and manipulate. 71 | */ 72 | protected XMLBuilder(Document xmlDocument) { 73 | super(xmlDocument); 74 | } 75 | 76 | /** 77 | * Construct a new builder object that wraps the given XML document and node. 78 | * This constructor is for internal use only. 79 | * 80 | * @param myNode 81 | * the XML node that this builder node will wrap. This node may 82 | * be part of the XML document, or it may be a new element that is to be 83 | * added to the document. 84 | * @param parentNode 85 | * If not null, the given myElement will be appended as child node of the 86 | * parentNode node. 87 | */ 88 | protected XMLBuilder(Node myNode, Node parentNode) { 89 | super(myNode, parentNode); 90 | } 91 | 92 | /** 93 | * Construct a builder for new XML document with a default namespace. 94 | * The document will be created with the given root element, and the builder 95 | * returned by this method will serve as the starting-point for any further 96 | * document additions. 97 | * 98 | * @param name 99 | * the name of the document's root element. 100 | * @param namespaceURI 101 | * default namespace URI for document, ignored if null or empty. 102 | * @param enableExternalEntities 103 | * enable external entities; beware of XML External Entity (XXE) injection. 104 | * @param isNamespaceAware 105 | * enable or disable namespace awareness in the underlying 106 | * {@link DocumentBuilderFactory} 107 | * @return 108 | * a builder node that can be used to add more nodes to the XML document. 109 | * 110 | * @throws FactoryConfigurationError xyz 111 | * @throws ParserConfigurationException xyz 112 | */ 113 | public static XMLBuilder create(String name, String namespaceURI, 114 | boolean enableExternalEntities, boolean isNamespaceAware) 115 | throws ParserConfigurationException, FactoryConfigurationError 116 | { 117 | return new XMLBuilder( 118 | createDocumentImpl( 119 | name, namespaceURI, enableExternalEntities, isNamespaceAware)); 120 | } 121 | 122 | /** 123 | * Construct a builder for new XML document. The document will be created 124 | * with the given root element, and the builder returned by this method 125 | * will serve as the starting-point for any further document additions. 126 | * 127 | * @param name 128 | * the name of the document's root element. 129 | * @param enableExternalEntities 130 | * enable external entities; beware of XML External Entity (XXE) injection. 131 | * @param isNamespaceAware 132 | * enable or disable namespace awareness in the underlying 133 | * {@link DocumentBuilderFactory} 134 | * @return 135 | * a builder node that can be used to add more nodes to the XML document. 136 | * 137 | * @throws FactoryConfigurationError xyz 138 | * @throws ParserConfigurationException xyz 139 | */ 140 | public static XMLBuilder create(String name, boolean enableExternalEntities, 141 | boolean isNamespaceAware) 142 | throws ParserConfigurationException, FactoryConfigurationError 143 | { 144 | return create(name, null, enableExternalEntities, isNamespaceAware); 145 | } 146 | 147 | /** 148 | * Construct a builder for new XML document with a default namespace. 149 | * The document will be created with the given root element, and the builder 150 | * returned by this method will serve as the starting-point for any further 151 | * document additions. 152 | * 153 | * @param name 154 | * the name of the document's root element. 155 | * @param namespaceURI 156 | * default namespace URI for document, ignored if null or empty. 157 | 158 | * @return 159 | * a builder node that can be used to add more nodes to the XML document. 160 | * 161 | * @throws FactoryConfigurationError xyz 162 | * @throws ParserConfigurationException xyz 163 | */ 164 | public static XMLBuilder create(String name, String namespaceURI) 165 | throws ParserConfigurationException, FactoryConfigurationError 166 | { 167 | return create(name, namespaceURI, false, true); 168 | } 169 | 170 | /** 171 | * Construct a builder for new XML document. The document will be created 172 | * with the given root element, and the builder returned by this method 173 | * will serve as the starting-point for any further document additions. 174 | * 175 | * @param name 176 | * the name of the document's root element. 177 | * @return 178 | * a builder node that can be used to add more nodes to the XML document. 179 | * 180 | * @throws FactoryConfigurationError xyz 181 | * @throws ParserConfigurationException xyz 182 | */ 183 | public static XMLBuilder create(String name) 184 | throws ParserConfigurationException, FactoryConfigurationError 185 | { 186 | return create(name, null); 187 | } 188 | 189 | /** 190 | * Construct a builder from an existing XML document. The provided XML 191 | * document will be parsed and an XMLBuilder object referencing the 192 | * document's root element will be returned. 193 | * 194 | * @param inputSource 195 | * an XML document input source that will be parsed into a DOM. 196 | * @param enableExternalEntities 197 | * enable external entities; beware of XML External Entity (XXE) injection. 198 | * @param isNamespaceAware 199 | * enable or disable namespace awareness in the underlying 200 | * {@link DocumentBuilderFactory} 201 | * @return 202 | * a builder node that can be used to add more nodes to the XML document. 203 | * 204 | * @throws ParserConfigurationException xyz 205 | * @throws FactoryConfigurationError xyz 206 | * @throws ParserConfigurationException xyz 207 | * @throws IOException xyz 208 | * @throws SAXException xyz 209 | */ 210 | public static XMLBuilder parse( 211 | InputSource inputSource, boolean enableExternalEntities, 212 | boolean isNamespaceAware) 213 | throws ParserConfigurationException, SAXException, IOException 214 | { 215 | return new XMLBuilder( 216 | parseDocumentImpl( 217 | inputSource, enableExternalEntities, isNamespaceAware)); 218 | } 219 | 220 | /** 221 | * Construct a builder from an existing XML document string. 222 | * The provided XML document will be parsed and an XMLBuilder 223 | * object referencing the document's root element will be returned. 224 | * 225 | * @param xmlString 226 | * an XML document string that will be parsed into a DOM. 227 | * @param enableExternalEntities 228 | * enable external entities; beware of XML External Entity (XXE) injection. 229 | * @param isNamespaceAware 230 | * enable or disable namespace awareness in the underlying 231 | * {@link DocumentBuilderFactory} 232 | * @return 233 | * a builder node that can be used to add more nodes to the XML document. 234 | * 235 | * @throws ParserConfigurationException xyz 236 | * @throws FactoryConfigurationError xyz 237 | * @throws ParserConfigurationException xyz 238 | * @throws IOException xyz 239 | * @throws SAXException xyz 240 | */ 241 | public static XMLBuilder parse( 242 | String xmlString, boolean enableExternalEntities, 243 | boolean isNamespaceAware) 244 | throws ParserConfigurationException, SAXException, IOException 245 | { 246 | return XMLBuilder.parse( 247 | new InputSource(new StringReader(xmlString)), 248 | enableExternalEntities, 249 | isNamespaceAware); 250 | } 251 | 252 | /** 253 | * Construct a builder from an existing XML document file. 254 | * The provided XML document will be parsed and an XMLBuilder 255 | * object referencing the document's root element will be returned. 256 | * 257 | * @param xmlFile 258 | * an XML document file that will be parsed into a DOM. 259 | * @param enableExternalEntities 260 | * enable external entities; beware of XML External Entity (XXE) injection. 261 | * @param isNamespaceAware 262 | * enable or disable namespace awareness in the underlying 263 | * {@link DocumentBuilderFactory} 264 | * @return 265 | * a builder node that can be used to add more nodes to the XML document. 266 | * 267 | * @throws ParserConfigurationException xyz 268 | * @throws FactoryConfigurationError xyz 269 | * @throws ParserConfigurationException xyz 270 | * @throws IOException xyz 271 | * @throws SAXException xyz 272 | */ 273 | public static XMLBuilder parse(File xmlFile, boolean enableExternalEntities, 274 | boolean isNamespaceAware) 275 | throws ParserConfigurationException, SAXException, IOException 276 | { 277 | return XMLBuilder.parse( 278 | new InputSource(new FileReader(xmlFile)), 279 | enableExternalEntities, 280 | isNamespaceAware); 281 | } 282 | 283 | /** 284 | * Construct a builder from an existing XML document. The provided XML 285 | * document will be parsed and an XMLBuilder object referencing the 286 | * document's root element will be returned. 287 | * 288 | * @param inputSource 289 | * an XML document input source that will be parsed into a DOM. 290 | * @return 291 | * a builder node that can be used to add more nodes to the XML document. 292 | * 293 | * @throws ParserConfigurationException xyz 294 | * @throws FactoryConfigurationError xyz 295 | * @throws ParserConfigurationException xyz 296 | * @throws IOException xyz 297 | * @throws SAXException xyz 298 | */ 299 | public static XMLBuilder parse(InputSource inputSource) 300 | throws ParserConfigurationException, SAXException, IOException 301 | { 302 | return XMLBuilder.parse(inputSource, false, true); 303 | } 304 | 305 | /** 306 | * Construct a builder from an existing XML document string. 307 | * The provided XML document will be parsed and an XMLBuilder 308 | * object referencing the document's root element will be returned. 309 | * 310 | * @param xmlString 311 | * an XML document string that will be parsed into a DOM. 312 | * @return 313 | * a builder node that can be used to add more nodes to the XML document. 314 | * 315 | * @throws ParserConfigurationException xyz 316 | * @throws FactoryConfigurationError xyz 317 | * @throws ParserConfigurationException xyz 318 | * @throws IOException xyz 319 | * @throws SAXException xyz 320 | */ 321 | public static XMLBuilder parse(String xmlString) 322 | throws ParserConfigurationException, SAXException, IOException 323 | { 324 | return XMLBuilder.parse(xmlString, false, true); 325 | } 326 | 327 | /** 328 | * Construct a builder from an existing XML document file. 329 | * The provided XML document will be parsed and an XMLBuilder 330 | * object referencing the document's root element will be returned. 331 | * 332 | * @param xmlFile 333 | * an XML document file that will be parsed into a DOM. 334 | * @return 335 | * a builder node that can be used to add more nodes to the XML document. 336 | * 337 | * @throws ParserConfigurationException xyz 338 | * @throws FactoryConfigurationError xyz 339 | * @throws ParserConfigurationException xyz 340 | * @throws IOException xyz 341 | * @throws SAXException xyz 342 | */ 343 | public static XMLBuilder parse(File xmlFile) 344 | throws ParserConfigurationException, SAXException, IOException 345 | { 346 | return XMLBuilder.parse(xmlFile, false, true); 347 | } 348 | 349 | @Override 350 | public XMLBuilder stripWhitespaceOnlyTextNodes() 351 | throws XPathExpressionException 352 | { 353 | super.stripWhitespaceOnlyTextNodesImpl(); 354 | return this; 355 | } 356 | 357 | @Override 358 | public XMLBuilder importXMLBuilder(BaseXMLBuilder builder) { 359 | super.importXMLBuilderImpl(builder); 360 | return this; 361 | } 362 | 363 | @Override 364 | public XMLBuilder root() { 365 | return new XMLBuilder(getDocument()); 366 | } 367 | 368 | @Override 369 | public XMLBuilder xpathFind(String xpath, NamespaceContext nsContext) 370 | throws XPathExpressionException 371 | { 372 | Node foundNode = super.xpathFindImpl(xpath, nsContext); 373 | return new XMLBuilder(foundNode, null); 374 | } 375 | 376 | @Override 377 | public XMLBuilder xpathFind(String xpath) throws XPathExpressionException { 378 | return xpathFind(xpath, null); 379 | } 380 | 381 | @Override 382 | public XMLBuilder element(String name) { 383 | String namespaceURI = super.lookupNamespaceURIImpl(name); 384 | return element(name, namespaceURI); 385 | } 386 | 387 | @Override 388 | public XMLBuilder elem(String name) { 389 | return element(name); 390 | } 391 | 392 | @Override 393 | public XMLBuilder e(String name) { 394 | return element(name); 395 | } 396 | 397 | @Override 398 | public XMLBuilder element(String name, String namespaceURI) { 399 | Element elem = super.elementImpl(name, namespaceURI); 400 | return new XMLBuilder(elem, this.getElement()); 401 | } 402 | 403 | @Override 404 | public XMLBuilder elementBefore(String name) { 405 | Element newElement = super.elementBeforeImpl(name); 406 | return new XMLBuilder(newElement, null); 407 | } 408 | 409 | @Override 410 | public XMLBuilder elementBefore(String name, String namespaceURI) { 411 | Element newElement = super.elementBeforeImpl(name, namespaceURI); 412 | return new XMLBuilder(newElement, null); 413 | } 414 | 415 | @Override 416 | public XMLBuilder attribute(String name, String value) { 417 | super.attributeImpl(name, value); 418 | return this; 419 | } 420 | 421 | @Override 422 | public XMLBuilder attr(String name, String value) { 423 | return attribute(name, value); 424 | } 425 | 426 | @Override 427 | public XMLBuilder a(String name, String value) { 428 | return attribute(name, value); 429 | } 430 | 431 | 432 | @Override 433 | public XMLBuilder text(String value, boolean replaceText) { 434 | super.textImpl(value, replaceText); 435 | return this; 436 | } 437 | 438 | @Override 439 | public XMLBuilder text(String value) { 440 | return this.text(value, false); 441 | } 442 | 443 | @Override 444 | public XMLBuilder t(String value) { 445 | return text(value); 446 | } 447 | 448 | @Override 449 | public XMLBuilder cdata(String data) { 450 | super.cdataImpl(data); 451 | return this; 452 | } 453 | 454 | @Override 455 | public XMLBuilder data(String data) { 456 | return cdata(data); 457 | } 458 | 459 | @Override 460 | public XMLBuilder d(String data) { 461 | return cdata(data); 462 | } 463 | 464 | @Override 465 | public XMLBuilder cdata(byte[] data) { 466 | super.cdataImpl(data); 467 | return this; 468 | } 469 | 470 | @Override 471 | public XMLBuilder data(byte[] data) { 472 | return cdata(data); 473 | } 474 | 475 | @Override 476 | public XMLBuilder d(byte[] data) { 477 | return cdata(data); 478 | } 479 | 480 | @Override 481 | public XMLBuilder comment(String comment) { 482 | super.commentImpl(comment); 483 | return this; 484 | } 485 | 486 | @Override 487 | public XMLBuilder cmnt(String comment) { 488 | return comment(comment); 489 | } 490 | 491 | @Override 492 | public XMLBuilder c(String comment) { 493 | return comment(comment); 494 | } 495 | 496 | @Override 497 | public XMLBuilder instruction(String target, String data) { 498 | super.instructionImpl(target, data); 499 | return this; 500 | } 501 | 502 | @Override 503 | public XMLBuilder inst(String target, String data) { 504 | return instruction(target, data); 505 | } 506 | 507 | @Override 508 | public XMLBuilder i(String target, String data) { 509 | return instruction(target, data); 510 | } 511 | 512 | @Override 513 | public XMLBuilder insertInstruction(String target, String data) { 514 | super.insertInstructionImpl(target, data); 515 | return this; 516 | } 517 | 518 | @Override 519 | public XMLBuilder reference(String name) { 520 | super.referenceImpl(name); 521 | return this; 522 | } 523 | 524 | @Override 525 | public XMLBuilder ref(String name) { 526 | return reference(name); 527 | } 528 | 529 | @Override 530 | public XMLBuilder r(String name) { 531 | return reference(name); 532 | } 533 | 534 | @Override 535 | public XMLBuilder namespace(String prefix, String namespaceURI) { 536 | super.namespaceImpl(prefix, namespaceURI); 537 | return this; 538 | } 539 | 540 | @Override 541 | public XMLBuilder ns(String prefix, String namespaceURI) { 542 | return attribute(prefix, namespaceURI); 543 | } 544 | 545 | @Override 546 | public XMLBuilder namespace(String namespaceURI) { 547 | this.namespace(null, namespaceURI); 548 | return this; 549 | } 550 | 551 | @Override 552 | public XMLBuilder ns(String namespaceURI) { 553 | return namespace(namespaceURI); 554 | } 555 | 556 | @Override 557 | public XMLBuilder up(int steps) { 558 | Node currNode = super.upImpl(steps); 559 | if (currNode instanceof Document) { 560 | return new XMLBuilder((Document) currNode); 561 | } else { 562 | return new XMLBuilder(currNode, null); 563 | } 564 | } 565 | 566 | @Override 567 | public XMLBuilder up() { 568 | return up(1); 569 | } 570 | 571 | @Override 572 | public XMLBuilder document() { 573 | return new XMLBuilder(getDocument(), null); 574 | } 575 | 576 | } 577 | -------------------------------------------------------------------------------- /src/main/java/com/jamesmurty/utils/XMLBuilder2.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2008-2020 James Murty (github.com/jmurty) 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 | * This code is available from the GitHub code repository at: 18 | * https://github.com/jmurty/java-xmlbuilder 19 | */ 20 | package com.jamesmurty.utils; 21 | 22 | import java.io.File; 23 | import java.io.FileNotFoundException; 24 | import java.io.FileReader; 25 | import java.io.IOException; 26 | import java.io.StringReader; 27 | import java.io.Writer; 28 | import java.util.Properties; 29 | 30 | import javax.xml.namespace.NamespaceContext; 31 | import javax.xml.namespace.QName; 32 | import javax.xml.parsers.DocumentBuilder; 33 | import javax.xml.parsers.DocumentBuilderFactory; 34 | import javax.xml.parsers.ParserConfigurationException; 35 | import javax.xml.transform.TransformerException; 36 | import javax.xml.xpath.XPathExpressionException; 37 | 38 | import org.w3c.dom.Document; 39 | import org.w3c.dom.Element; 40 | import org.w3c.dom.Node; 41 | import org.xml.sax.InputSource; 42 | import org.xml.sax.SAXException; 43 | 44 | /** 45 | * XML Builder is a utility that creates simple XML documents using relatively 46 | * sparse Java code. It is intended to allow for quick and painless creation of 47 | * XML documents where you might otherwise be tempted to use concatenated 48 | * strings, rather than face the tedium and verbosity of coding with 49 | * JAXP (http://jaxp.dev.java.net/). 50 | *

51 | * Internally, XML Builder uses JAXP to build a standard W3C 52 | * {@link org.w3c.dom.Document} model (DOM) that you can easily export as a 53 | * string, or access and manipulate further if you have special requirements. 54 | *

55 | *

56 | * The XMLBuilder2 class serves as a wrapper of {@link org.w3c.dom.Element} nodes, 57 | * and provides a number of utility methods that make it simple to 58 | * manipulate the underlying element and the document to which it belongs. 59 | * In essence, this class performs dual roles: it represents a specific XML 60 | * node, and also allows manipulation of the entire underlying XML document. 61 | * The platform's default {@link DocumentBuilderFactory} and 62 | * {@link DocumentBuilder} classes are used to build the document. 63 | *

64 | *

65 | * XMLBuilder2 has an feature set to the original XMLBuilder, but only ever 66 | * throws runtime exceptions (as opposed to checked exceptions). Any internal 67 | * checked exceptions are caught and wrapped in an 68 | * {@link XMLBuilderRuntimeException} object. 69 | *

70 | * 71 | * @author James Murty 72 | */ 73 | public final class XMLBuilder2 extends BaseXMLBuilder { 74 | 75 | /** 76 | * Construct a new builder object that wraps the given XML document. 77 | * This constructor is for internal use only. 78 | * 79 | * @param xmlDocument 80 | * an XML document that the builder will manage and manipulate. 81 | */ 82 | protected XMLBuilder2(Document xmlDocument) { 83 | super(xmlDocument); 84 | } 85 | 86 | /** 87 | * Construct a new builder object that wraps the given XML document and node. 88 | * This constructor is for internal use only. 89 | * 90 | * @param myNode 91 | * the XML node that this builder node will wrap. This node may 92 | * be part of the XML document, or it may be a new element that is to be 93 | * added to the document. 94 | * @param parentNode 95 | * If not null, the given myElement will be appended as child node of the 96 | * parentNode node. 97 | */ 98 | protected XMLBuilder2(Node myNode, Node parentNode) { 99 | super(myNode, parentNode); 100 | } 101 | 102 | private static RuntimeException wrapExceptionAsRuntimeException(Exception e) { 103 | // Don't wrap (or re-wrap) runtime exceptions. 104 | if (e instanceof RuntimeException) { 105 | return (RuntimeException) e; 106 | } else { 107 | return new XMLBuilderRuntimeException(e); 108 | } 109 | } 110 | 111 | /** 112 | * Construct a builder for new XML document with a default namespace. 113 | * The document will be created with the given root element, and the builder 114 | * returned by this method will serve as the starting-point for any further 115 | * document additions. 116 | * 117 | * @param name 118 | * the name of the document's root element. 119 | * @param namespaceURI 120 | * default namespace URI for document, ignored if null or empty. 121 | * @param enableExternalEntities 122 | * enable external entities; beware of XML External Entity (XXE) injection. 123 | * @param isNamespaceAware 124 | * enable or disable namespace awareness in the underlying 125 | * {@link DocumentBuilderFactory} 126 | * @return 127 | * a builder node that can be used to add more nodes to the XML document. 128 | * @throws XMLBuilderRuntimeException 129 | * to wrap {@link ParserConfigurationException} 130 | */ 131 | public static XMLBuilder2 create( 132 | String name, String namespaceURI, boolean enableExternalEntities, 133 | boolean isNamespaceAware) 134 | { 135 | try { 136 | return new XMLBuilder2( 137 | createDocumentImpl( 138 | name, namespaceURI, enableExternalEntities, isNamespaceAware)); 139 | } catch (ParserConfigurationException e) { 140 | throw wrapExceptionAsRuntimeException(e); 141 | } 142 | } 143 | 144 | /** 145 | * Construct a builder for new XML document. The document will be created 146 | * with the given root element, and the builder returned by this method 147 | * will serve as the starting-point for any further document additions. 148 | * 149 | * @param name 150 | * the name of the document's root element. 151 | * @param enableExternalEntities 152 | * enable external entities; beware of XML External Entity (XXE) injection. 153 | * @param isNamespaceAware 154 | * enable or disable namespace awareness in the underlying 155 | * {@link DocumentBuilderFactory} 156 | * @return 157 | * a builder node that can be used to add more nodes to the XML document. 158 | * @throws XMLBuilderRuntimeException 159 | * to wrap {@link ParserConfigurationException} 160 | */ 161 | public static XMLBuilder2 create(String name, 162 | boolean enableExternalEntities, boolean isNamespaceAware) 163 | { 164 | return XMLBuilder2.create( 165 | name, null, enableExternalEntities, isNamespaceAware); 166 | } 167 | 168 | /** 169 | * Construct a builder for new XML document with a default namespace. 170 | * The document will be created with the given root element, and the builder 171 | * returned by this method will serve as the starting-point for any further 172 | * document additions. 173 | * 174 | * @param name 175 | * the name of the document's root element. 176 | * @param namespaceURI 177 | * default namespace URI for document, ignored if null or empty. 178 | * @return 179 | * a builder node that can be used to add more nodes to the XML document. 180 | * @throws XMLBuilderRuntimeException 181 | * to wrap {@link ParserConfigurationException} 182 | */ 183 | public static XMLBuilder2 create(String name, String namespaceURI) 184 | { 185 | return XMLBuilder2.create(name, namespaceURI, false, true); 186 | } 187 | 188 | /** 189 | * Construct a builder for new XML document. The document will be created 190 | * with the given root element, and the builder returned by this method 191 | * will serve as the starting-point for any further document additions. 192 | * 193 | * @param name 194 | * the name of the document's root element. 195 | * @return 196 | * a builder node that can be used to add more nodes to the XML document. 197 | * @throws XMLBuilderRuntimeException 198 | * to wrap {@link ParserConfigurationException} 199 | */ 200 | public static XMLBuilder2 create(String name) 201 | { 202 | return XMLBuilder2.create(name, null, false, true); 203 | } 204 | 205 | /** 206 | * Construct a builder from an existing XML document. The provided XML 207 | * document will be parsed and an XMLBuilder2 object referencing the 208 | * document's root element will be returned. 209 | * 210 | * @param inputSource 211 | * an XML document input source that will be parsed into a DOM. 212 | * @param enableExternalEntities 213 | * enable external entities; beware of XML External Entity (XXE) injection. 214 | * @param isNamespaceAware 215 | * enable or disable namespace awareness in the underlying 216 | * {@link DocumentBuilderFactory} 217 | * @return 218 | * a builder node that can be used to add more nodes to the XML document. 219 | * @throws XMLBuilderRuntimeException 220 | * to wrap {@link ParserConfigurationException}, {@link SAXException}, 221 | * {@link IOException} 222 | */ 223 | public static XMLBuilder2 parse( 224 | InputSource inputSource, boolean enableExternalEntities, 225 | boolean isNamespaceAware) 226 | { 227 | try { 228 | return new XMLBuilder2( 229 | parseDocumentImpl( 230 | inputSource, enableExternalEntities, isNamespaceAware)); 231 | } catch (ParserConfigurationException e) { 232 | throw wrapExceptionAsRuntimeException(e); 233 | } catch (SAXException e) { 234 | throw wrapExceptionAsRuntimeException(e); 235 | } catch (IOException e) { 236 | throw wrapExceptionAsRuntimeException(e); 237 | } 238 | } 239 | 240 | /** 241 | * Construct a builder from an existing XML document string. 242 | * The provided XML document will be parsed and an XMLBuilder2 243 | * object referencing the document's root element will be returned. 244 | * 245 | * @param xmlString 246 | * an XML document string that will be parsed into a DOM. 247 | * @param enableExternalEntities 248 | * enable external entities; beware of XML External Entity (XXE) injection. 249 | * @param isNamespaceAware 250 | * enable or disable namespace awareness in the underlying 251 | * {@link DocumentBuilderFactory} 252 | * @return 253 | * a builder node that can be used to add more nodes to the XML document. 254 | */ 255 | public static XMLBuilder2 parse( 256 | String xmlString, boolean enableExternalEntities, boolean isNamespaceAware) 257 | { 258 | return XMLBuilder2.parse( 259 | new InputSource(new StringReader(xmlString)), 260 | enableExternalEntities, 261 | isNamespaceAware); 262 | } 263 | 264 | /** 265 | * Construct a builder from an existing XML document file. 266 | * The provided XML document will be parsed and an XMLBuilder2 267 | * object referencing the document's root element will be returned. 268 | * 269 | * @param xmlFile 270 | * an XML document file that will be parsed into a DOM. 271 | * @param enableExternalEntities 272 | * enable external entities; beware of XML External Entity (XXE) injection. 273 | * @param isNamespaceAware 274 | * enable or disable namespace awareness in the underlying 275 | * {@link DocumentBuilderFactory} 276 | * @return 277 | * a builder node that can be used to add more nodes to the XML document. 278 | * @throws XMLBuilderRuntimeException 279 | * to wrap {@link ParserConfigurationException}, {@link SAXException}, 280 | * {@link IOException}, {@link FileNotFoundException} 281 | */ 282 | public static XMLBuilder2 parse(File xmlFile, boolean enableExternalEntities, 283 | boolean isNamespaceAware) 284 | { 285 | try { 286 | return XMLBuilder2.parse( 287 | new InputSource(new FileReader(xmlFile)), 288 | enableExternalEntities, 289 | isNamespaceAware); 290 | } catch (FileNotFoundException e) { 291 | throw wrapExceptionAsRuntimeException(e); 292 | } 293 | } 294 | 295 | /** 296 | * Construct a builder from an existing XML document. The provided XML 297 | * document will be parsed and an XMLBuilder2 object referencing the 298 | * document's root element will be returned. 299 | * 300 | * @param inputSource 301 | * an XML document input source that will be parsed into a DOM. 302 | * @return 303 | * a builder node that can be used to add more nodes to the XML document. 304 | * @throws XMLBuilderRuntimeException 305 | * to wrap {@link ParserConfigurationException}, {@link SAXException}, 306 | * {@link IOException} 307 | */ 308 | public static XMLBuilder2 parse(InputSource inputSource) 309 | { 310 | return XMLBuilder2.parse(inputSource, false, true); 311 | } 312 | 313 | /** 314 | * Construct a builder from an existing XML document string. 315 | * The provided XML document will be parsed and an XMLBuilder2 316 | * object referencing the document's root element will be returned. 317 | * 318 | * @param xmlString 319 | * an XML document string that will be parsed into a DOM. 320 | * @return 321 | * a builder node that can be used to add more nodes to the XML document. 322 | */ 323 | public static XMLBuilder2 parse(String xmlString) 324 | { 325 | return XMLBuilder2.parse(xmlString, false, true); 326 | } 327 | 328 | /** 329 | * Construct a builder from an existing XML document file. 330 | * The provided XML document will be parsed and an XMLBuilder2 331 | * object referencing the document's root element will be returned. 332 | * 333 | * @param xmlFile 334 | * an XML document file that will be parsed into a DOM. 335 | * @return 336 | * a builder node that can be used to add more nodes to the XML document. 337 | * @throws XMLBuilderRuntimeException 338 | * to wrap {@link ParserConfigurationException}, {@link SAXException}, 339 | * {@link IOException}, {@link FileNotFoundException} 340 | */ 341 | public static XMLBuilder2 parse(File xmlFile) 342 | { 343 | return XMLBuilder2.parse(xmlFile, false, true); 344 | } 345 | 346 | /** 347 | * @throws XMLBuilderRuntimeException 348 | * to wrap {@link XPathExpressionException} 349 | */ 350 | @Override 351 | public XMLBuilder2 stripWhitespaceOnlyTextNodes() 352 | { 353 | try { 354 | super.stripWhitespaceOnlyTextNodesImpl(); 355 | return this; 356 | } catch (XPathExpressionException e) { 357 | throw wrapExceptionAsRuntimeException(e); 358 | } 359 | } 360 | 361 | @Override 362 | public XMLBuilder2 importXMLBuilder(BaseXMLBuilder builder) { 363 | super.importXMLBuilderImpl(builder); 364 | return this; 365 | } 366 | 367 | @Override 368 | public XMLBuilder2 root() { 369 | return new XMLBuilder2(getDocument()); 370 | } 371 | 372 | /** 373 | * @throws XMLBuilderRuntimeException 374 | * to wrap {@link XPathExpressionException} 375 | */ 376 | @Override 377 | public XMLBuilder2 xpathFind(String xpath, NamespaceContext nsContext) 378 | { 379 | try { 380 | Node foundNode = super.xpathFindImpl(xpath, nsContext); 381 | return new XMLBuilder2(foundNode, null); 382 | } catch (XPathExpressionException e) { 383 | throw wrapExceptionAsRuntimeException(e); 384 | } 385 | } 386 | 387 | @Override 388 | public XMLBuilder2 xpathFind(String xpath) { 389 | return xpathFind(xpath, null); 390 | } 391 | 392 | @Override 393 | public XMLBuilder2 element(String name) { 394 | String namespaceURI = super.lookupNamespaceURIImpl(name); 395 | return element(name, namespaceURI); 396 | } 397 | 398 | @Override 399 | public XMLBuilder2 elem(String name) { 400 | return element(name); 401 | } 402 | 403 | @Override 404 | public XMLBuilder2 e(String name) { 405 | return element(name); 406 | } 407 | 408 | @Override 409 | public XMLBuilder2 element(String name, String namespaceURI) { 410 | Element elem = super.elementImpl(name, namespaceURI); 411 | return new XMLBuilder2(elem, this.getElement()); 412 | } 413 | 414 | @Override 415 | public XMLBuilder2 elementBefore(String name) { 416 | Element newElement = super.elementBeforeImpl(name); 417 | return new XMLBuilder2(newElement, null); 418 | } 419 | 420 | @Override 421 | public XMLBuilder2 elementBefore(String name, String namespaceURI) { 422 | Element newElement = super.elementBeforeImpl(name, namespaceURI); 423 | return new XMLBuilder2(newElement, null); 424 | } 425 | 426 | @Override 427 | public XMLBuilder2 attribute(String name, String value) { 428 | super.attributeImpl(name, value); 429 | return this; 430 | } 431 | 432 | @Override 433 | public XMLBuilder2 attr(String name, String value) { 434 | return attribute(name, value); 435 | } 436 | 437 | @Override 438 | public XMLBuilder2 a(String name, String value) { 439 | return attribute(name, value); 440 | } 441 | 442 | 443 | @Override 444 | public XMLBuilder2 text(String value, boolean replaceText) { 445 | super.textImpl(value, replaceText); 446 | return this; 447 | } 448 | 449 | @Override 450 | public XMLBuilder2 text(String value) { 451 | return this.text(value, false); 452 | } 453 | 454 | @Override 455 | public XMLBuilder2 t(String value) { 456 | return text(value); 457 | } 458 | 459 | @Override 460 | public XMLBuilder2 cdata(String data) { 461 | super.cdataImpl(data); 462 | return this; 463 | } 464 | 465 | @Override 466 | public XMLBuilder2 data(String data) { 467 | return cdata(data); 468 | } 469 | 470 | @Override 471 | public XMLBuilder2 d(String data) { 472 | return cdata(data); 473 | } 474 | 475 | @Override 476 | public XMLBuilder2 cdata(byte[] data) { 477 | super.cdataImpl(data); 478 | return this; 479 | } 480 | 481 | @Override 482 | public XMLBuilder2 data(byte[] data) { 483 | return cdata(data); 484 | } 485 | 486 | @Override 487 | public XMLBuilder2 d(byte[] data) { 488 | return cdata(data); 489 | } 490 | 491 | @Override 492 | public XMLBuilder2 comment(String comment) { 493 | super.commentImpl(comment); 494 | return this; 495 | } 496 | 497 | @Override 498 | public XMLBuilder2 cmnt(String comment) { 499 | return comment(comment); 500 | } 501 | 502 | @Override 503 | public XMLBuilder2 c(String comment) { 504 | return comment(comment); 505 | } 506 | 507 | @Override 508 | public XMLBuilder2 instruction(String target, String data) { 509 | super.instructionImpl(target, data); 510 | return this; 511 | } 512 | 513 | @Override 514 | public XMLBuilder2 inst(String target, String data) { 515 | return instruction(target, data); 516 | } 517 | 518 | @Override 519 | public XMLBuilder2 i(String target, String data) { 520 | return instruction(target, data); 521 | } 522 | 523 | @Override 524 | public XMLBuilder2 insertInstruction(String target, String data) { 525 | super.insertInstructionImpl(target, data); 526 | return this; 527 | } 528 | 529 | @Override 530 | public XMLBuilder2 reference(String name) { 531 | super.referenceImpl(name); 532 | return this; 533 | } 534 | 535 | @Override 536 | public XMLBuilder2 ref(String name) { 537 | return reference(name); 538 | } 539 | 540 | @Override 541 | public XMLBuilder2 r(String name) { 542 | return reference(name); 543 | } 544 | 545 | @Override 546 | public XMLBuilder2 namespace(String prefix, String namespaceURI) { 547 | super.namespaceImpl(prefix, namespaceURI); 548 | return this; 549 | } 550 | 551 | @Override 552 | public XMLBuilder2 ns(String prefix, String namespaceURI) { 553 | return namespace(prefix, namespaceURI); 554 | } 555 | 556 | @Override 557 | public XMLBuilder2 namespace(String namespaceURI) { 558 | this.namespace(null, namespaceURI); 559 | return this; 560 | } 561 | 562 | @Override 563 | public XMLBuilder2 ns(String namespaceURI) { 564 | return namespace(namespaceURI); 565 | } 566 | 567 | @Override 568 | public XMLBuilder2 up(int steps) { 569 | Node currNode = super.upImpl(steps); 570 | if (currNode instanceof Document) { 571 | return new XMLBuilder2((Document) currNode); 572 | } else { 573 | return new XMLBuilder2(currNode, null); 574 | } 575 | } 576 | 577 | @Override 578 | public XMLBuilder2 up() { 579 | return up(1); 580 | } 581 | 582 | @Override 583 | public XMLBuilder2 document() { 584 | return new XMLBuilder2(getDocument(), null); 585 | } 586 | 587 | /** 588 | * @throws XMLBuilderRuntimeException 589 | * to wrap {@link TransformerException} 590 | * 591 | */ 592 | @Override 593 | public String asString() { 594 | try { 595 | return super.asString(); 596 | } catch (TransformerException e) { 597 | throw wrapExceptionAsRuntimeException(e); 598 | } 599 | } 600 | 601 | /** 602 | * @throws XMLBuilderRuntimeException 603 | * to wrap {@link TransformerException} 604 | * 605 | */ 606 | @Override 607 | public String asString(Properties properties) { 608 | try { 609 | return super.asString(properties); 610 | } catch (TransformerException e) { 611 | throw wrapExceptionAsRuntimeException(e); 612 | } 613 | } 614 | 615 | /** 616 | * @throws XMLBuilderRuntimeException 617 | * to wrap {@link TransformerException} 618 | * 619 | */ 620 | @Override 621 | public String elementAsString() { 622 | try { 623 | return super.elementAsString(); 624 | } catch (TransformerException e) { 625 | throw wrapExceptionAsRuntimeException(e); 626 | } 627 | } 628 | 629 | /** 630 | * @throws XMLBuilderRuntimeException 631 | * to wrap {@link TransformerException} 632 | * 633 | */ 634 | @Override 635 | public String elementAsString(Properties outputProperties) { 636 | try { 637 | return super.elementAsString(outputProperties); 638 | } catch (TransformerException e) { 639 | throw wrapExceptionAsRuntimeException(e); 640 | } 641 | } 642 | 643 | /** 644 | * @throws XMLBuilderRuntimeException 645 | * to wrap {@link TransformerException} 646 | * 647 | */ 648 | @Override 649 | public void toWriter(boolean wholeDocument, Writer writer, Properties outputProperties) 650 | { 651 | try { 652 | super.toWriter(wholeDocument, writer, outputProperties); 653 | } catch (TransformerException e) { 654 | throw wrapExceptionAsRuntimeException(e); 655 | } 656 | } 657 | 658 | /** 659 | * @throws XMLBuilderRuntimeException 660 | * to wrap {@link TransformerException} 661 | * 662 | */ 663 | @Override 664 | public void toWriter(Writer writer, Properties outputProperties) 665 | { 666 | try { 667 | super.toWriter(writer, outputProperties); 668 | } catch (TransformerException e) { 669 | throw wrapExceptionAsRuntimeException(e); 670 | } 671 | } 672 | 673 | /** 674 | * @throws XMLBuilderRuntimeException 675 | * to wrap {@link XPathExpressionException} 676 | * 677 | */ 678 | @Override 679 | public Object xpathQuery(String xpath, QName type, NamespaceContext nsContext) 680 | { 681 | try { 682 | return super.xpathQuery(xpath, type, nsContext); 683 | } catch (XPathExpressionException e) { 684 | throw wrapExceptionAsRuntimeException(e); 685 | } 686 | } 687 | 688 | /** 689 | * @throws XMLBuilderRuntimeException 690 | * to wrap {@link XPathExpressionException} 691 | * 692 | */ 693 | @Override 694 | public Object xpathQuery(String xpath, QName type) 695 | { 696 | try { 697 | return super.xpathQuery(xpath, type); 698 | } catch (XPathExpressionException e) { 699 | throw wrapExceptionAsRuntimeException(e); 700 | } 701 | } 702 | 703 | } 704 | -------------------------------------------------------------------------------- /src/main/java/com/jamesmurty/utils/XMLBuilderRuntimeException.java: -------------------------------------------------------------------------------- 1 | package com.jamesmurty.utils; 2 | 3 | /** 4 | * A runtime exception class used in {@link XMLBuilder2} to wrap any exceptions 5 | * that would otherwise lead to checked exceptions in the interface. 6 | * 7 | * @author jmurty 8 | * 9 | */ 10 | public class XMLBuilderRuntimeException extends RuntimeException { 11 | 12 | private static final long serialVersionUID = -635323496745601589L; 13 | 14 | /** 15 | * @param exception 16 | * cause exception to be wrapped 17 | */ 18 | public XMLBuilderRuntimeException(Exception exception) { 19 | super(exception); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/com/jamesmurty/utils/BaseXMLBuilderTests.java: -------------------------------------------------------------------------------- 1 | package com.jamesmurty.utils; 2 | 3 | import java.io.File; 4 | import java.io.FileWriter; 5 | import java.io.StringReader; 6 | import java.io.StringWriter; 7 | import java.util.Properties; 8 | 9 | import javax.xml.transform.OutputKeys; 10 | import javax.xml.xpath.XPathConstants; 11 | import javax.xml.xpath.XPathExpressionException; 12 | 13 | import junit.framework.TestCase; 14 | import net.iharder.Base64; 15 | 16 | import org.w3c.dom.Node; 17 | import org.w3c.dom.NodeList; 18 | import org.xml.sax.InputSource; 19 | 20 | public abstract class BaseXMLBuilderTests extends TestCase { 21 | 22 | public static final String EXAMPLE_XML_DOC_START = 23 | "" + 24 | "" + 25 | "http://code.google.com/p/java-xmlbuilder/" + 26 | "" + 27 | "" + 28 | "http://jets3t.s3.amazonaws.com/index.html"; 29 | 30 | public static final String EXAMPLE_XML_DOC_END = 31 | "" + 32 | ""; 33 | 34 | public static final String EXAMPLE_XML_DOC = EXAMPLE_XML_DOC_START + EXAMPLE_XML_DOC_END; 35 | 36 | protected abstract Class XMLBuilderToTest() throws Exception; 37 | 38 | protected abstract boolean isRuntimeExceptionsOnly(); 39 | 40 | protected BaseXMLBuilder XMLBuilder_create(String name) throws Exception { 41 | return (BaseXMLBuilder) XMLBuilderToTest().getMethod( 42 | "create", String.class).invoke(null, name); 43 | } 44 | 45 | protected BaseXMLBuilder XMLBuilder_create(String name, String nsURI) throws Exception { 46 | return (BaseXMLBuilder) XMLBuilderToTest().getMethod( 47 | "create", String.class, String.class).invoke(null, name, nsURI); 48 | } 49 | 50 | protected BaseXMLBuilder XMLBuilder_parse(InputSource source) throws Exception { 51 | return (BaseXMLBuilder) XMLBuilderToTest().getMethod( 52 | "parse", InputSource.class).invoke(null, source); 53 | } 54 | 55 | protected BaseXMLBuilder XMLBuilder_parse( 56 | String documentString, boolean enableExternalEntities, 57 | boolean isNamespaceAware) throws Exception 58 | { 59 | return (BaseXMLBuilder) XMLBuilderToTest().getMethod( 60 | "parse", String.class, boolean.class, boolean.class).invoke( 61 | null, documentString, enableExternalEntities, isNamespaceAware); 62 | } 63 | 64 | protected BaseXMLBuilder XMLBuilder_parse(String documentString) throws Exception { 65 | return (BaseXMLBuilder) XMLBuilderToTest().getMethod( 66 | "parse", String.class).invoke(null, documentString); 67 | } 68 | 69 | public void testXmlDocumentCreation() throws Exception { 70 | /* Build XML document in-place */ 71 | BaseXMLBuilder builder = XMLBuilder_create("Projects") 72 | .e("java-xmlbuilder") 73 | .a("language", "Java") 74 | .a("scm","SVN") 75 | .e("Location") 76 | .a("type", "URL") 77 | .t("http://code.google.com/p/java-xmlbuilder/") 78 | .up() 79 | .up() 80 | .e("JetS3t") 81 | .a("language", "Java") 82 | .a("scm","CVS") 83 | .e("Location") 84 | .a("type", "URL") 85 | .t("http://jets3t.s3.amazonaws.com/index.html"); 86 | 87 | /* Set output properties */ 88 | Properties outputProperties = new Properties(); 89 | // Explicitly identify the output as an XML document 90 | outputProperties.put(javax.xml.transform.OutputKeys.METHOD, "xml"); 91 | // Pretty-print the XML output (doesn't work in all cases) 92 | outputProperties.put(javax.xml.transform.OutputKeys.INDENT, "no"); 93 | // Omit the XML declaration, which can differ depending on the test's run context. 94 | outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes"); 95 | 96 | /* Serialize builder document */ 97 | StringWriter writer = new StringWriter(); 98 | builder.toWriter(writer, outputProperties); 99 | 100 | assertEquals(EXAMPLE_XML_DOC, writer.toString()); 101 | 102 | /* Build XML document in segments*/ 103 | BaseXMLBuilder projectsB = XMLBuilder_create("Projects"); 104 | projectsB.e("java-xmlbuilder") 105 | .a("language", "Java") 106 | .a("scm","SVN") 107 | .e("Location") 108 | .a("type", "URL") 109 | .t("http://code.google.com/p/java-xmlbuilder/"); 110 | BaseXMLBuilder jets3tB = projectsB.e("JetS3t") 111 | .a("language", "Java") 112 | .a("scm","CVS"); 113 | jets3tB.e("Location") 114 | .a("type", "URL") 115 | .t("http://jets3t.s3.amazonaws.com/index.html"); 116 | 117 | assertEquals(builder.asString(), projectsB.asString()); 118 | } 119 | 120 | public void testParseAndXPath() throws Exception { 121 | // Parse an existing XML document 122 | BaseXMLBuilder builder = XMLBuilder_parse( 123 | new InputSource(new StringReader(EXAMPLE_XML_DOC))); 124 | assertEquals("Projects", builder.root().getElement().getNodeName()); 125 | assertEquals("Invalid current element", "Projects", builder.getElement().getNodeName()); 126 | 127 | // Find the first Location element 128 | builder = builder.xpathFind("//Location"); 129 | assertEquals("Location", builder.getElement().getNodeName()); 130 | assertEquals("http://code.google.com/p/java-xmlbuilder/", 131 | builder.getElement().getTextContent()); 132 | 133 | // Find JetS3t's Location element 134 | builder = builder.xpathFind("//JetS3t/Location"); 135 | assertEquals("Location", builder.getElement().getNodeName()); 136 | assertEquals("http://jets3t.s3.amazonaws.com/index.html", 137 | builder.getElement().getTextContent()); 138 | 139 | // Find the project with the scm attribute 'CVS' (should be JetS3t) 140 | builder = builder.xpathFind("//*[@scm = 'CVS']"); 141 | assertEquals("JetS3t", builder.getElement().getNodeName()); 142 | 143 | // Try an invalid XPath that does not resolve to an element 144 | try { 145 | builder.xpathFind("//@language"); 146 | fail("Non-Element XPath expression should have failed"); 147 | } catch (Exception e) { 148 | if (isRuntimeExceptionsOnly()) { 149 | assertEquals(XMLBuilderRuntimeException.class, e.getClass()); 150 | e = (Exception) e.getCause(); 151 | } 152 | assertEquals(XPathExpressionException.class, e.getClass()); 153 | assertTrue(e.getMessage().contains("does not resolve to an Element")); 154 | } 155 | 156 | /* Perform full-strength XPath queries that do not have to 157 | * resolve to an Element, and do not return BaseXMLBuilder instances 158 | */ 159 | 160 | // Find the Location value for the JetS3t project 161 | String location = (String) builder.xpathQuery( 162 | "//JetS3t/Location/.", XPathConstants.STRING); 163 | assertEquals("http://jets3t.s3.amazonaws.com/index.html", location); 164 | 165 | // Count the number of projects (count returned as String) 166 | String countAsString = (String) builder.xpathQuery( 167 | "count(/Projects/*)", XPathConstants.STRING); 168 | assertEquals("2", countAsString); 169 | 170 | // Count the number of projects (count returned as "Number" - actually Double) 171 | Number countAsNumber = (Number) builder.xpathQuery( 172 | "count(/Projects/*)", XPathConstants.NUMBER); 173 | assertEquals(2.0, countAsNumber); 174 | 175 | // Find all nodes under Projects 176 | NodeList nodes = (NodeList) builder.xpathQuery( 177 | "/Projects/*", XPathConstants.NODESET); 178 | assertEquals(2, nodes.getLength()); 179 | assertEquals("JetS3t", nodes.item(1).getNodeName()); 180 | 181 | // Returns null if nothing found when a NODE type is requested... 182 | assertNull(builder.xpathQuery("//WrongName", XPathConstants.NODE)); 183 | // ... or an empty String if a STRING type is requested... 184 | assertEquals("", builder.xpathQuery("//WrongName", XPathConstants.STRING)); 185 | // ... or NaN if a NUMBER type is requested... 186 | assertEquals(Double.NaN, builder.xpathQuery("//WrongName", XPathConstants.NUMBER)); 187 | 188 | /* Add a new XML element at a specific XPath location in an existing document */ 189 | 190 | // Use XPath to get a builder at the insert location 191 | BaseXMLBuilder xpathLocB = builder.xpathFind("//JetS3t"); 192 | assertEquals("JetS3t", xpathLocB.getElement().getNodeName()); 193 | 194 | // Append a new element with the location's builder 195 | BaseXMLBuilder location2B = xpathLocB.elem("Location2").attr("type", "Testing"); 196 | assertEquals("Location2", location2B.getElement().getNodeName()); 197 | assertEquals("JetS3t", location2B.up().getElement().getNodeName()); 198 | assertEquals(xpathLocB.getElement(), location2B.up().getElement()); 199 | assertEquals(builder.root(), location2B.root()); 200 | 201 | // Sanity-check the entire resultant XML document 202 | Properties outputProperties = new Properties(); 203 | outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes"); 204 | String xmlAsString = location2B.asString(outputProperties); 205 | 206 | assertFalse(EXAMPLE_XML_DOC.equals(xmlAsString)); 207 | assertTrue(xmlAsString.contains("")); 208 | assertEquals( 209 | EXAMPLE_XML_DOC_START + "" + EXAMPLE_XML_DOC_END, 210 | xmlAsString); 211 | } 212 | 213 | public void testParseAndAmendDocWithWhitespaceNodes() throws Exception { 214 | // Parse example XML document and output with indenting, to add whitespace nodes 215 | Properties outputProperties = new Properties(); 216 | outputProperties.put(OutputKeys.INDENT, "yes"); 217 | outputProperties.put("{http://xml.apache.org/xslt}indent-amount", "2"); 218 | String xmlWithWhitespaceNodes = 219 | XMLBuilder_parse(EXAMPLE_XML_DOC).asString(outputProperties); 220 | 221 | // Re-parse document that now has whitespace nodes 222 | BaseXMLBuilder builder = XMLBuilder_parse(xmlWithWhitespaceNodes); 223 | 224 | // Ensure we can add a node to the document (re issue #17) 225 | builder.xpathFind("//JetS3t") 226 | .elem("AnotherLocation").attr("type", "Testing"); 227 | String xmlWithAmendments = builder.asString(outputProperties); 228 | assertTrue(xmlWithAmendments.contains("")); 229 | } 230 | 231 | public void testStripWhitespaceNodesFromDocument() throws Exception { 232 | // Parse example XML document and output with indenting, to add whitespace nodes 233 | Properties outputProperties = new Properties(); 234 | outputProperties.put(OutputKeys.INDENT, "yes"); 235 | outputProperties.put("{http://xml.apache.org/xslt}indent-amount", "2"); 236 | String xmlWithWhitespaceNodes = 237 | XMLBuilder_parse(EXAMPLE_XML_DOC).asString(outputProperties); 238 | 239 | // Re-parse document that now has whitespace text nodes 240 | BaseXMLBuilder builder = XMLBuilder_parse(xmlWithWhitespaceNodes); 241 | assertTrue(builder.asString().contains("\n")); 242 | assertTrue(builder.asString().contains(" ")); 243 | 244 | // Strip whitespace nodes 245 | builder.stripWhitespaceOnlyTextNodes(); 246 | assertFalse(builder.asString().contains("\n")); 247 | assertFalse(builder.asString().contains(" ")); 248 | } 249 | 250 | public void testSimpleXpath() throws Exception { 251 | String xmlDoc = ""; 252 | BaseXMLBuilder builder = XMLBuilder_parse(xmlDoc); 253 | BaseXMLBuilder builderNode = builder.xpathFind("report_objects"); 254 | assertTrue("report_objects".equals(builderNode.getElement().getNodeName())); 255 | assertTrue("".equals(builderNode.elementAsString())); 256 | } 257 | 258 | /** 259 | * Test for issue #11: https://code.google.com/p/java-xmlbuilder/issues/detail?id=11 260 | * @throws Exception 261 | */ 262 | public void testAddElementsInLoop() throws Exception { 263 | BaseXMLBuilder builder = XMLBuilder_create("DocRoot"); 264 | BaseXMLBuilder parentBuilder = builder.element("Parent"); 265 | 266 | // Add set of elements to Parent using a loop... 267 | for (int i = 1; i <= 10; i++) { 268 | parentBuilder.elem("IntegerValue" + i).text("" + i); 269 | } 270 | 271 | // ...and confirm element set is within parent after a call to up() 272 | parentBuilder.up(); 273 | 274 | assertEquals("Parent", parentBuilder.getElement().getNodeName()); 275 | assertEquals("DocRoot", builder.getElement().getNodeName()); 276 | assertEquals(1, builder.getElement().getChildNodes().getLength()); 277 | assertEquals("Parent", builder.getElement().getChildNodes().item(0).getNodeName()); 278 | assertEquals(10, parentBuilder.getElement().getChildNodes().getLength()); 279 | assertEquals("IntegerValue1", parentBuilder.getElement().getChildNodes().item(0).getNodeName()); 280 | assertEquals("1", parentBuilder.getElement().getChildNodes().item(0).getTextContent()); 281 | } 282 | 283 | public void testTraversalDuringBuild() throws Exception { 284 | BaseXMLBuilder builder = XMLBuilder_create("ElemDepth1") 285 | .e("ElemDepth2") 286 | .e("ElemDepth3") 287 | .e("ElemDepth4"); 288 | assertEquals("ElemDepth3", builder.up().getElement().getNodeName()); 289 | assertEquals("ElemDepth1", builder.up(3).getElement().getNodeName()); 290 | // Traverse too far up the node tree... 291 | assertEquals("ElemDepth1", builder.up(4).getElement().getNodeName()); 292 | // Traverse way too far up the node tree... 293 | assertEquals("ElemDepth1", builder.up(100).getElement().getNodeName()); 294 | } 295 | 296 | public void testImport() throws Exception { 297 | BaseXMLBuilder importer = XMLBuilder_create("Importer") 298 | .elem("Imported") 299 | .elem("Element") 300 | .elem("Goes").attr("are-we-there-yet", "almost") 301 | .elem("Here"); 302 | BaseXMLBuilder importee = XMLBuilder_create("Importee") 303 | .elem("Importee").attr("awating-my", "new-home") 304 | .elem("IsEntireSubtree") 305 | .elem("Included"); 306 | importer.importXMLBuilder(importee); 307 | 308 | // Ensure we're at the same point in the XML doc 309 | assertEquals("Here", importer.getElement().getNodeName()); 310 | 311 | try { 312 | importer.xpathFind("//Importee"); 313 | importer.xpathFind("//IsEntireSubtree"); 314 | importer.xpathFind("//IsEntireSubtree"); 315 | importer.xpathFind("//Included"); 316 | } catch (XPathExpressionException e) { 317 | fail("XMLBuilder import failed: " + e.getMessage()); 318 | } 319 | 320 | BaseXMLBuilder invalidImporter = XMLBuilder_create("InvalidImporter") 321 | .text("BadBadBad"); 322 | try { 323 | invalidImporter.importXMLBuilder(importee); 324 | fail("Should not be able to import XMLBuilder into " 325 | + "an element containing text nodes"); 326 | } catch (IllegalStateException e) { 327 | // Expected 328 | } 329 | } 330 | 331 | public void testCDataNodes() throws Exception { 332 | String text = "Text data -- left as it is"; 333 | String textForBytes = "Byte data is automatically base64-encoded"; 334 | String textEncoded = Base64.encodeBytes(textForBytes.getBytes("UTF-8")); 335 | 336 | BaseXMLBuilder builder = XMLBuilder_create("TestCDataNodes") 337 | .elem("CDataTextElem") 338 | .cdata(text) 339 | .up() 340 | .elem("CDataBytesElem") 341 | .cdata(textForBytes.getBytes("UTF-8")); 342 | 343 | Node cdataTextNode = builder.xpathFind("//CDataTextElem") 344 | .getElement().getChildNodes().item(0); 345 | assertEquals(Node.CDATA_SECTION_NODE, cdataTextNode.getNodeType()); 346 | assertEquals(text, cdataTextNode.getNodeValue()); 347 | 348 | Node cdataBytesNode = builder.xpathFind("//CDataBytesElem") 349 | .getElement().getChildNodes().item(0); 350 | assertEquals(Node.CDATA_SECTION_NODE, cdataBytesNode.getNodeType()); 351 | assertEquals(textEncoded, cdataBytesNode.getNodeValue()); 352 | String base64Decoded = new String(Base64.decode(cdataBytesNode.getNodeValue())); 353 | assertEquals(textForBytes, base64Decoded); 354 | } 355 | 356 | public void testElementAsString() throws Exception { 357 | BaseXMLBuilder builder = XMLBuilder_create("This") 358 | .elem("Is").elem("My").text("Test"); 359 | // By default, entire XML document is serialized regardless of starting-point 360 | assertEquals("Test", builder.asString()); 361 | assertEquals("Test", builder.xpathFind("//My").asString()); 362 | // Serialize a specific Element and its descendants with elementAsString 363 | assertEquals("Test", builder.xpathFind("//My").elementAsString()); 364 | } 365 | 366 | public void testNamespaces() throws Exception { 367 | BaseXMLBuilder builder = XMLBuilder_create("NamespaceTest", "urn:default") 368 | .namespace("prefix1", "urn:ns1") 369 | 370 | .element("NSDefaultImplicit").up() 371 | .element("NSDefaultExplicit", "urn:default").up() 372 | 373 | .element("NS1Explicit", "urn:ns1").up() 374 | .element("prefix1:NS1WithPrefixExplicit", "urn:ns1").up() 375 | .element("prefix1:NS1WithPrefixImplicit").up(); 376 | 377 | // Build a namespace context from the builder's document 378 | NamespaceContextImpl context = builder.buildDocumentNamespaceContext(); 379 | 380 | // All elements in a namespaced document inherit a namespace URI, 381 | // for namespaced document any non-namespaced XPath query will fail. 382 | try { 383 | builder.xpathFind("//:NSDefaultImplicit"); 384 | fail("Namespaced xpath query without context is invalid"); 385 | } catch (Exception e) { 386 | if (isRuntimeExceptionsOnly()) { 387 | assertEquals(XMLBuilderRuntimeException.class, e.getClass()); 388 | e = (Exception) e.getCause(); 389 | } 390 | assertEquals(XPathExpressionException.class, e.getClass()); 391 | } 392 | try { 393 | builder.xpathFind("//NSDefaultImplicit", context); 394 | fail("XPath query without prefixes on namespaced docs is invalid"); 395 | } catch (Exception e) { 396 | if (isRuntimeExceptionsOnly()) { 397 | assertEquals(XMLBuilderRuntimeException.class, e.getClass()); 398 | e = (Exception) e.getCause(); 399 | } 400 | assertEquals(XPathExpressionException.class, e.getClass()); 401 | } 402 | 403 | // Find nodes with default namespace 404 | builder.xpathFind("/:NamespaceTest", context); 405 | builder.xpathFind("//:NSDefaultImplicit", context); 406 | builder.xpathFind("//:NSDefaultExplicit", context); 407 | 408 | // Must use namespace-aware xpath to find namespaced nodes 409 | try { 410 | builder.xpathFind("//NSDefaultExplicit"); 411 | fail(); 412 | } catch (Exception e) { 413 | if (isRuntimeExceptionsOnly()) { 414 | assertEquals(XMLBuilderRuntimeException.class, e.getClass()); 415 | e = (Exception) e.getCause(); 416 | } 417 | assertEquals(XPathExpressionException.class, e.getClass()); 418 | } 419 | try { 420 | builder.xpathFind("//:NSDefaultExplicit"); 421 | fail(); 422 | } catch (Exception e) { 423 | if (isRuntimeExceptionsOnly()) { 424 | assertEquals(XMLBuilderRuntimeException.class, e.getClass()); 425 | e = (Exception) e.getCause(); 426 | } 427 | assertEquals(XPathExpressionException.class, e.getClass()); 428 | } 429 | try { 430 | builder.xpathFind("//NSDefaultExplicit", context); 431 | fail(); 432 | } catch (Exception e) { 433 | if (isRuntimeExceptionsOnly()) { 434 | assertEquals(XMLBuilderRuntimeException.class, e.getClass()); 435 | e = (Exception) e.getCause(); 436 | } 437 | assertEquals(XPathExpressionException.class, e.getClass()); 438 | } 439 | 440 | // Find node with namespace prefix 441 | builder.xpathFind("//prefix1:NS1Explicit", context); 442 | builder.xpathFind("//prefix1:NS1WithPrefixExplicit", context); 443 | builder.xpathFind("//prefix1:NS1WithPrefixImplicit", context); 444 | 445 | // Find nodes with user-defined prefix "aliases" 446 | context.addNamespace("default-alias", "urn:default"); 447 | context.addNamespace("prefix1-alias", "urn:ns1"); 448 | builder.xpathFind("//default-alias:NSDefaultExplicit", context); 449 | builder.xpathFind("//prefix1-alias:NS1Explicit", context); 450 | 451 | // User can override context mappings, for better or worse 452 | context.addNamespace("", "urn:default"); 453 | builder.xpathFind("//:NSDefaultExplicit", context); 454 | 455 | context.addNamespace("", "urn:wrong"); 456 | try { 457 | builder.xpathFind("//:NSDefaultExplicit", context); 458 | fail(); 459 | } catch (Exception e) { 460 | if (isRuntimeExceptionsOnly()) { 461 | assertEquals(XMLBuilderRuntimeException.class, e.getClass()); 462 | e = (Exception) e.getCause(); 463 | } 464 | assertEquals(XPathExpressionException.class, e.getClass()); 465 | } 466 | 467 | // Users are not prevented from creating elements that reference 468 | // an undefined namespace prefix -- user beware 469 | builder.element("undefined-prefix:ElementName"); 470 | } 471 | 472 | public void testNamespaceUnawareBuilder() throws Exception { 473 | String XML_WITH_NAMESPACES = 474 | "" + 475 | "Found me" + 476 | ""; 477 | 478 | // Builder set to be unaware of namespaces can traverse DOM with 479 | // namespaces without using namespace prefixes 480 | BaseXMLBuilder result = XMLBuilder_parse( 481 | XML_WITH_NAMESPACES, 482 | false, // enableExternalEntities 483 | false // isNamespaceAware 484 | ).xpathFind("/NamespaceUnwareTest/NestedElement"); 485 | assertEquals("Found me", result.getElement().getTextContent()); 486 | 487 | // Builder set to be aware of namespaces (per default) cannot traverse 488 | // DOM with namespaces without using namespace prefixes 489 | try { 490 | result = XMLBuilder_parse(XML_WITH_NAMESPACES) 491 | .xpathFind("/NamespaceUnwareTest/NestedElement"); 492 | } catch (Exception ex) { 493 | Throwable cause = null; 494 | if (this instanceof TestXMLBuilder2) { 495 | cause = ex.getCause(); // Exception wrapped in runtime ex 496 | } else { 497 | cause = ex; 498 | } 499 | assertEquals( 500 | cause.getClass(), XPathExpressionException.class); 501 | assertTrue( 502 | cause.getMessage().contains( 503 | "XPath expression \"/NamespaceUnwareTest/NestedElement\"" 504 | + " does not resolve to an Element in context" 505 | )); 506 | } 507 | assertEquals("Found me", result.getElement().getTextContent()); 508 | } 509 | 510 | public void testElementBefore() throws Exception { 511 | BaseXMLBuilder builder = XMLBuilder_create("TestDocument", "urn:default") 512 | .namespace("custom", "urn:custom") 513 | .elem("Before").up() 514 | .elem("After"); 515 | NamespaceContextImpl context = builder.buildDocumentNamespaceContext(); 516 | 517 | // Ensure XML structure is correct before insert 518 | assertEquals("" 519 | + "", builder.asString()); 520 | 521 | // Insert an element before the "After" element, no explicit namespace (will use default) 522 | BaseXMLBuilder testDoc = XMLBuilder_parse(builder.asString()) 523 | .xpathFind("/:TestDocument/:After", context); 524 | BaseXMLBuilder insertedBuilder = testDoc.elementBefore("Inserted"); 525 | assertEquals("Inserted", insertedBuilder.getElement().getNodeName()); 526 | assertEquals("" 527 | + "", testDoc.asString()); 528 | 529 | // Insert another element, this time with a custom namespace prefix 530 | insertedBuilder = insertedBuilder.elementBefore("custom:InsertedAgain"); 531 | assertEquals("custom:InsertedAgain", insertedBuilder.getElement().getNodeName()); 532 | assertEquals("" 533 | + "", 534 | testDoc.asString()); 535 | 536 | // Insert another element, this time with a custom namespace ref 537 | insertedBuilder = insertedBuilder.elementBefore("InsertedYetAgain", "urn:custom2"); 538 | assertEquals("InsertedYetAgain", insertedBuilder.getElement().getNodeName()); 539 | assertEquals("" 540 | + "" 541 | + "", 542 | testDoc.asString()); 543 | } 544 | 545 | public void testTextNodes() throws Exception { 546 | BaseXMLBuilder builder = XMLBuilder_create("TestDocument") 547 | .elem("TextElement") 548 | .text("Initial"); 549 | 550 | BaseXMLBuilder textElementBuilder = builder.xpathFind("//TextElement"); 551 | assertEquals("Initial", textElementBuilder.getElement().getTextContent()); 552 | 553 | // By default, text methods append value to existing text 554 | textElementBuilder.text("Appended"); 555 | assertEquals("InitialAppended", textElementBuilder.getElement().getTextContent()); 556 | 557 | // Use boolean flag to replace text nodes with a new value 558 | textElementBuilder.text("Replacement", true); 559 | assertEquals("Replacement", textElementBuilder.getElement().getTextContent()); 560 | 561 | // Fail-fast if a null text value is provided. 562 | try { 563 | textElementBuilder.text(null); 564 | fail("null text value should cause IllegalArgumentException"); 565 | } catch (IllegalArgumentException ex) { 566 | assertEquals("Illegal null text value", ex.getMessage()); 567 | } 568 | 569 | try { 570 | textElementBuilder.text(null, true); 571 | fail("null text value should cause IllegalArgumentException"); 572 | } catch (IllegalArgumentException ex) { 573 | assertEquals("Illegal null text value", ex.getMessage()); 574 | } 575 | 576 | } 577 | 578 | public void testProcessingInstructionNodes() throws Exception { 579 | // Add instruction to root document element node (usual append-in-node behaviour) 580 | BaseXMLBuilder builder = XMLBuilder_create("TestDocument").instruction("test", "data"); 581 | assertEquals("", builder.asString()); 582 | 583 | // Add instruction after the root document element (not within it) 584 | builder = XMLBuilder_create("TestDocument3").document().instruction("test", "data"); 585 | assertEquals("", builder.asString().trim()); 586 | 587 | // Insert instruction as first node of the root document 588 | builder = XMLBuilder_create("TestDocument3").insertInstruction("test", "data"); 589 | assertEquals( 590 | "", 591 | // Remove newlines from XML as this differs across platforms 592 | builder.asString().replace("\n", "")); 593 | 594 | // Insert instruction as first node of the root document, second example 595 | builder = XMLBuilder_create("TestDocument4").elem("ChildElem") 596 | .root().insertInstruction("test", "data"); 597 | assertEquals( 598 | "", 599 | // Remove newlines from XML as this differs across platforms 600 | builder.asString().replace("\n", "")); 601 | } 602 | 603 | /** 604 | * Test for strange issue raised by user on comments form where OutputKeys.STANDALONE setting 605 | * in transformer is ignored. 606 | * 607 | * @throws Exception 608 | */ 609 | public void testSetStandaloneToYes() throws Exception { 610 | String xmlDoc = ""; 611 | BaseXMLBuilder builder = XMLBuilder_parse( 612 | new InputSource(new StringReader(xmlDoc))); 613 | 614 | // Basic output settings 615 | Properties outputProperties = new Properties(); 616 | outputProperties.put(javax.xml.transform.OutputKeys.VERSION, "1.0"); 617 | outputProperties.put(javax.xml.transform.OutputKeys.METHOD, "xml"); 618 | outputProperties.put(javax.xml.transform.OutputKeys.ENCODING, "UTF-8"); 619 | 620 | // Use Document@setXmlStandalone(true) to ensure OutputKeys.STANDALONE is respected. 621 | builder.getDocument().setXmlStandalone(true); 622 | outputProperties.put(javax.xml.transform.OutputKeys.STANDALONE, "yes"); 623 | 624 | /* Serialize builder document */ 625 | StringWriter writer = new StringWriter(); 626 | builder.toWriter(writer, outputProperties); 627 | 628 | assertEquals( 629 | "" + xmlDoc, 630 | writer.toString()); 631 | } 632 | 633 | /** 634 | * Test the {@link BaseXMLBuilder#asString(Properties)} method output a document 635 | * of the correct size when the document is moderately large, re issue 636 | * #1 (on GitHub). 637 | * 638 | * @throws Exception 639 | */ 640 | public void testModerateDocumentSizeAsString() throws Exception { 641 | // Create a moderate document around 0.5 MB 642 | long expectedByteSize = 505021; 643 | BaseXMLBuilder builder = XMLBuilder_create("RootNode"); 644 | for (int i = 0; i < 5000; i++) { 645 | builder 646 | .e("TreeRoot") 647 | .e("TreeTrunk") 648 | .e("TreeBranch") 649 | .e("TreeLeaf") 650 | .t("Some Aphids"); 651 | 652 | } 653 | // Omit XML declaration, which will otherwise be included in file 654 | // via #toWriter but not in string via #asString 655 | Properties outputProperties = new Properties(); 656 | outputProperties.put( 657 | javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes"); 658 | // Ensure XML as string has expected length... 659 | String xmlString = builder.asString(outputProperties); 660 | assertEquals(expectedByteSize, xmlString.length()); 661 | // ...and matches size of XML written to file 662 | File f = File.createTempFile( 663 | "java-xmlbuilder-testmoderatedocumentsizeasstring", ".xml"); 664 | builder.toWriter(new FileWriter(f), outputProperties); 665 | assertEquals(expectedByteSize, f.length()); 666 | f.delete(); 667 | } 668 | 669 | /** 670 | * Ensure XML Builder parse methods use a default configuration that 671 | * prevents XML External Entity (XXE) injection attacks. 672 | * 673 | * @throws Exception 674 | */ 675 | public void testXMLBuilderParserImmuneToXXEAttackByDefault() throws Exception { 676 | String externalFilePath = "src/test/java/com/jamesmurty/utils/external.txt"; 677 | File externalFile = new File(externalFilePath); 678 | String XML_DOC_WITH_XXE = 679 | "" + 680 | "" + 682 | " ]>" + 683 | EXAMPLE_XML_DOC_START + "&xx1;" + EXAMPLE_XML_DOC_END; 684 | // By default, builder is immune from XXE injection 685 | BaseXMLBuilder builder = XMLBuilder_parse(XML_DOC_WITH_XXE); 686 | String parsedXml = builder.asString(); 687 | assertFalse(parsedXml.indexOf("Injected XXE Data") >= 0); 688 | // If you enable external entity processing, builder becomes subject to XXE injection 689 | builder = XMLBuilder_parse(XML_DOC_WITH_XXE, true, true); 690 | parsedXml = builder.asString(); 691 | assertTrue(parsedXml.indexOf("Injected XXE Data") >= 0); 692 | } 693 | 694 | } 695 | -------------------------------------------------------------------------------- /src/test/java/com/jamesmurty/utils/TestXMLBuilder.java: -------------------------------------------------------------------------------- 1 | package com.jamesmurty.utils; 2 | 3 | public class TestXMLBuilder extends BaseXMLBuilderTests { 4 | 5 | @Override 6 | public Class XMLBuilderToTest() throws Exception { 7 | return XMLBuilder.class; 8 | } 9 | 10 | @Override 11 | protected boolean isRuntimeExceptionsOnly() { 12 | return false; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/com/jamesmurty/utils/TestXMLBuilder2.java: -------------------------------------------------------------------------------- 1 | package com.jamesmurty.utils; 2 | 3 | import javax.xml.xpath.XPathConstants; 4 | import javax.xml.xpath.XPathExpressionException; 5 | 6 | public class TestXMLBuilder2 extends BaseXMLBuilderTests { 7 | 8 | @Override 9 | public Class XMLBuilderToTest() throws Exception { 10 | return XMLBuilder2.class; 11 | } 12 | 13 | @Override 14 | protected boolean isRuntimeExceptionsOnly() { 15 | return true; 16 | } 17 | 18 | // NOTE: No checked exceptions for API calls made in this test method 19 | public void testNoCheckedExceptions() { 20 | XMLBuilder2 builder = XMLBuilder2.create("Blah"); 21 | builder = XMLBuilder2.parse(EXAMPLE_XML_DOC); 22 | builder.stripWhitespaceOnlyTextNodes(); 23 | builder.asString(); 24 | builder.elementAsString(); 25 | builder.xpathQuery("/*", XPathConstants.NODESET); 26 | builder.xpathFind("/Projects"); 27 | } 28 | 29 | public void testExceptionWrappedInXMLBuilderRuntimeException() { 30 | XMLBuilder2 builder = XMLBuilder2.parse(EXAMPLE_XML_DOC); 31 | try { 32 | builder.xpathFind("/BadPath"); 33 | fail("Expected XMLBuilderRuntimeException"); 34 | } catch (XMLBuilderRuntimeException e) { 35 | assertEquals(XMLBuilderRuntimeException.class, e.getClass()); 36 | Throwable cause = e.getCause(); 37 | assertEquals(XPathExpressionException.class, cause.getClass()); 38 | assertTrue(cause.getMessage().contains("does not resolve to an Element")); 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/com/jamesmurty/utils/external.txt: -------------------------------------------------------------------------------- 1 | Injected XXE Data --------------------------------------------------------------------------------