├── .github └── workflows │ ├── maven.yml │ └── release.yml ├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── LICENSE ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml ├── xjx-sax ├── pom.xml └── src │ ├── main │ └── java │ │ └── io │ │ └── jonasg │ │ └── xjx │ │ ├── Attributes.java │ │ ├── BufferedPositionedReader.java │ │ ├── EndTag.java │ │ ├── PositionedReader.java │ │ ├── StartTag.java │ │ ├── Token.java │ │ ├── TokenEmitter.java │ │ ├── Tokenizer.java │ │ ├── sax │ │ ├── Attribute.java │ │ ├── SaxHandler.java │ │ └── SaxParser.java │ │ └── scanners │ │ ├── CDATAScanner.java │ │ ├── CharacterScanner.java │ │ ├── CommentBodyScanner.java │ │ ├── DocumentTypeDeclarationScanner.java │ │ ├── EndTagScanner.java │ │ ├── Scanner.java │ │ ├── StartCommentScanner.java │ │ ├── StartTagScanner.java │ │ ├── WhiteSpaceScanner.java │ │ └── XmlParsingException.java │ └── test │ └── java │ └── io │ └── jonasg │ └── xjx │ ├── BufferedPositionedReaderTest.java │ ├── TokenizerIntegrationTest.java │ ├── sax │ ├── SaxParserTest.java │ └── TestSaxHandler.java │ └── scanners │ ├── CDATAScannerTest.java │ ├── CharacterScannerTest.java │ ├── CommentBodyScannerTest.java │ ├── DocumentTypeDeclarationScannerTest.java │ ├── EndTagScannerTest.java │ └── StartTagScannerTest.java └── xjx-serdes ├── pom.xml └── src ├── main └── java │ └── io │ └── jonasg │ └── xjx │ └── serdes │ ├── Path.java │ ├── Section.java │ ├── Tag.java │ ├── TypeMappers.java │ ├── XjxSerdes.java │ ├── deserialize │ ├── DeserializationFeature.java │ ├── LazySupplier.java │ ├── MapAsRoot.java │ ├── MapOf.java │ ├── MapRootSaxHandler.java │ ├── MapWithTypeInfo.java │ ├── PathBasedSaxHandler.java │ ├── PathWriter.java │ ├── PathWriterIndex.java │ ├── PathWriterIndexFactory.java │ ├── RecordWrapper.java │ ├── TagPath.java │ ├── TypedValueMapSaxHandler.java │ ├── ValueDeserialization.java │ ├── ValueDeserializationHandler.java │ ├── ValueDeserializer.java │ ├── XjxDeserializationException.java │ ├── accessor │ │ ├── FieldAccessor.java │ │ ├── RecordFieldAccessor.java │ │ ├── ReflectiveFieldAccessor.java │ │ └── SetterFieldAccessor.java │ └── config │ │ ├── ConfigurationBuilder.java │ │ └── XjxConfiguration.java │ ├── reflector │ ├── FieldReflector.java │ ├── InstanceField.java │ ├── InstanceReflector.java │ ├── Reflector.java │ └── TypeReflector.java │ └── serialize │ ├── XmlNode.java │ ├── XmlNodeStructureFactory.java │ └── XmlStringBuilder.java └── test └── java └── io └── jonasg └── xjx └── serdes ├── deserialize ├── ComplexTypeDeserializationTest.java ├── CustomValueDeserializationTest.java ├── DataTypeDeserializationTest.java ├── DeserializationFieldAccessTest.java ├── DeserializationInstanceCreationTest.java ├── EnumDeserializationTest.java ├── GeneralDeserializationTest.java ├── JavaRecordDeserializationTest.java ├── ListDeserializationTest.java ├── SetDeserializationTest.java └── TagAttributeDeserializationTest.java └── serialize ├── EnumSerializationTest.java ├── GeneralSerializationTest.java └── TagAttributeSerializationTest.java /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Verify Code 10 | 11 | on: 12 | push: 13 | branches: [ "main" ] 14 | pull_request: 15 | branches: [ "main" ] 16 | 17 | jobs: 18 | build: 19 | 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Set up JDK 17 25 | uses: actions/setup-java@v3 26 | with: 27 | java-version: '17' 28 | distribution: 'temurin' 29 | cache: maven 30 | - name: Build with Maven 31 | run: mvn -B verify --file pom.xml 32 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | tag: 7 | type: string 8 | description: tag 9 | 10 | jobs: 11 | bump_version: 12 | name: "Bump Version" 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Set up Java 17 | uses: actions/setup-java@v3 18 | with: 19 | java-version: '17' 20 | distribution: 'adopt' 21 | - name: Bump version 22 | id: bump 23 | run: "./mvnw versions:set -DnewVersion=${{ github.event.inputs.tag }} -DprocessAllModules && ./mvnw versions:commit -DprocessAllModules" 24 | - name: Commit bumped version 25 | uses: stefanzweifel/git-auto-commit-action@v5 26 | with: 27 | commit_message: "Prepare release version ${{ github.event.inputs.tag }}" 28 | publish: 29 | name: "Publish" 30 | needs: [bump_version] 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v3 34 | with: 35 | fetch-depth: 0 36 | ref: main 37 | - name: Set up Java 38 | uses: actions/setup-java@v3 39 | with: 40 | java-version: '17' 41 | distribution: 'adopt' 42 | - name: Stage Release 43 | run: ./mvnw -Prelease deploy -DaltDeploymentRepository=local::file:./target/staging-deploy 44 | - name: Release 45 | env: 46 | JRELEASER_NEXUS2_USERNAME: ${{ secrets.JRELEASER_NEXUS2_USERNAME }} 47 | JRELEASER_NEXUS2_PASSWORD: ${{ secrets.JRELEASER_NEXUS2_PASSWORD }} 48 | JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }} 49 | JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }} 50 | JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.JRELEASER_GPG_PUBLIC_KEY }} 51 | JRELEASER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | run: ./mvnw -Prelease jreleaser:full-release --non-recursive 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **big.xml 2 | **gpx.xml 3 | target/ 4 | !.mvn/wrapper/maven-wrapper.jar 5 | !**/src/main/**/target/ 6 | !**/src/test/**/target/ 7 | 8 | ### Maven ### 9 | .flattened-pom.xml 10 | 11 | ### Xjx ### 12 | LargeFileTest.java 13 | TODO.md 14 | 15 | ### IntelliJ IDEA ### 16 | .idea/ 17 | *.iws 18 | *.iml 19 | *.ipr 20 | 21 | ### Eclipse ### 22 | .apt_generated 23 | .classpath 24 | .factorypath 25 | .project 26 | .settings 27 | .springBeans 28 | .sts4-cache 29 | 30 | ### NetBeans ### 31 | /nbproject/private/ 32 | /nbbuild/ 33 | /dist/ 34 | /nbdist/ 35 | /.nb-gradle/ 36 | build/ 37 | !**/src/main/**/build/ 38 | !**/src/test/**/build/ 39 | 40 | ### VS Code ### 41 | .vscode/ 42 | 43 | ### Mac OS ### 44 | .DS_Store 45 | 46 | ### Jreleaser 47 | out/ 48 | trace.log 49 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonas-grgt/xjx/1643168f6aeb38321a9ae1ee4ddc2309be0a6805/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.4/apache-maven-3.9.4-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🙅 Xjx [![Maven Central](https://img.shields.io/maven-central/v/io.jonasg/xjx-serdes.svg)](https://search.maven.org/artifact/io.jonasg/xjx-serdes) [![License](https://img.shields.io/github/license/jonas-grgt/xjx.svg)](https://opensource.org/licenses/Apache-2.0) 2 | Java - XML serializing and deserializing (serdes) library: No Dependencies, Just Simplicity 3 | 4 | # 🤔 Why 5 | The "why" behind Xjx is rooted in the necessity for a minimalist, actively maintained XML-to-Java and vice versa library. 6 | 7 | # 📦 Modules 8 | Xjx exists out of two modules: 9 | - **xjx-sax** a standalone SAX like parser 10 | - **xjx-serdes** XML to Java deserializer and serializer 11 | 12 | # 🔑 Key Features 13 | - Explicitly map fields to specific tags using `@Tag` 14 | - Select specific tags using an **XPath** like expression `@Tag(path = "/WeatherData/Location/City)` 15 | - Out-of-the-box support for most common data types (including enums, collections, and maps) 16 | - Explicit deserialization of values using `@ValueDeserialization` 17 | - Support for records 18 | 19 | # ✨ xjx-serdes 20 | 21 | Contains the XML serializer and deserializer. 22 | 23 | ## ⚙️ Installation 24 | 25 | ```xml 26 | 27 | io.jonasg 28 | xjx-serdes 29 | ${xjx.version} 30 | 31 | ``` 32 | 33 | ## 🔆 Quick sample usage 34 | Getters and setters are omitted for brevity, 35 | but setters are **not required** as Xjx can also reside to using reflection 36 | but will **always** favor direct access through setters over it. 37 | ```java 38 | public class WeatherData { 39 | private WeatherData() { 40 | } 41 | 42 | @Tag(path = "/WeatherData/Location") 43 | private Location location; 44 | 45 | @Tag(path = "/WeatherData/CurrentConditions/Temperature/Value") 46 | private Integer temperature; 47 | } 48 | 49 | public class Location { 50 | private Location() { 51 | } 52 | 53 | @Tag(path = "Country", attribute = "City") 54 | private String City; 55 | 56 | @Tag(path = "Country") 57 | private String Country; 58 | } 59 | ``` 60 | 61 | ```java 62 | String document = """ 63 | 64 | 65 | 66 | USA 67 | 68 | 69 | 70 | 75 71 | 72 | 73 | 74 | 60 75 | % 76 | 77 | Sunny 78 | 79 | """; 80 | 81 | 82 | var xjx = new XjxSerdes(); 83 | WeatherData weatherData = xjx.read(document, WeatherData.class); 84 | 85 | String xmlDocument = xjx.write(weatherData); 86 | ``` 87 | ## General deserialization rules 88 | Deserialization is guided by the use of the `@Tag` annotation. Fields annotated with `@Tag` are candidates for deserialization, while unannotated fields are ignored. 89 | 90 | `@Tag` annotation is mandatory for fields considered for deserialization. 91 | Each `@Tag` annotation must include a `path` property, using an XPath-like expression to map the field within the XML document. 92 | 93 | ### Path Expressions 94 | 95 | Path expressions can be **absolute**, starting with a slash, representing a path from the root tag to the mapped tag. 96 | **Relative** paths, without a starting slash, require a parent to be mapped absolutely. 97 | Root mappings can be placed top-level on the class, all subsequent relative mappings are relative to the root mapping. 98 | 99 | ```java 100 | import java.math.BigDecimal; 101 | 102 | @Tag(path = "/WeatherData") 103 | class Weather { 104 | // not annotated with @Tag hence is ignored 105 | String id; 106 | 107 | // example for an absolute mapped tag 108 | @Tag(path = "/WeatherData/Location") 109 | Location location; 110 | 111 | // example for a relative-mapped tag based upon the top-level mapping 112 | @Tag(path = "CurrentConditions") 113 | Conditions conditions; 114 | 115 | // normally all fields without @Tag are ignored, yet this field is 116 | // taken into account because at least one of its child fields is 117 | // annotated with @Tag 118 | Temperature temperature; 119 | } 120 | 121 | class Location { 122 | // example for a relative mapped tag 123 | @Tag(path = "city") 124 | String city; 125 | 126 | // a combination of relative and absolute mapped tags is possible 127 | // within a nested object 128 | @Tag(path = "/WeatherData/CurrenConditions/CurrenConditions") 129 | String condition; 130 | } 131 | 132 | class Temperature { 133 | // absolute mapped tag 134 | @Tag(path = "/WeatherData/CurrenConditions/Temperature") 135 | BigDecimal max; 136 | } 137 | ``` 138 | 139 | Absolute mapping a field of top-level class containing a top-level root mapping is supported. 140 | ```java 141 | @Tag(path = "/WeatherData") 142 | class Weather { 143 | @Tag(path = "/WeatherData/Location") 144 | Location location; 145 | } 146 | 147 | ``` 148 | 149 | ### Attributes 150 | 151 | Attributes can be mapped using the `attribute` property of the `@Tag` annotation. 152 | 153 | ```xml 154 | 155 | 156 | John 157 | 158 | ``` 159 | 160 | ```java 161 | public class Person { 162 | @Tag(path = "/Person/Name") 163 | String name; 164 | 165 | @Tag(path = "/Person/Name", attribute = "sex") 166 | String sex; 167 | 168 | @Tag(path = "/Person/Name", attribute = "age") 169 | int age; 170 | } 171 | ``` 172 | 173 | ### Enum types 174 | 175 | Xjx offers straightforward and efficient deserialization support for Enum types. 176 | When mapping XML character data to Enum fields in Java, Xjx matches the character data with the names of the Enum constants. 177 | Deserialization Rules for Enums 178 | 179 | - Direct Name Matching: The deserializer matches the XML character data directly with the names of the Enum constants. The match is case-sensitive. 180 | - Defaulting to Null: If the XML character data does not match any Enum constant names, the field is set to null. This is the default behavior when a match cannot be established. 181 | 182 | ### Collection types 183 | When deserializing an XML document containing repeated elements, it can be mapped onto one of the collection types `List` or `Set`. 184 | 185 | The following conventions should be followed: 186 | 187 | - Only `List` and `Set` types are supported for mapping repeated elements. 188 | - The `@Tag` annotation should be used on a `List` or `Set` field. 189 | - Include a `path` attribute pointing to the containing tag that holds the repeated tags. 190 | - Include an `items` attribute pointing to the repeated tag, relatively. 191 | - The `path` attribute supports both relative and absolute paths. 192 | - The generic argument can be any standard simple type (e.g., `String`, `Boolean`, `Double`, `Long`, etc.) or a custom complex type. 193 | - Fields within the nested complex type can be annotated as usual, using relative or absolute paths. 194 | 195 | Example XML document: 196 | 197 | ```xml 198 | 199 | 200 | 201 | 202 | 203 | 71 204 | 205 | 206 | 62 207 | 208 | 209 | 210 | 211 | 78 212 | 213 | 214 | 71 215 | 216 | 217 | 218 | 219 | ``` 220 | 221 | ### Map mixed tags within a container to multiple collections 222 | 223 | Xjx is able to map repeated mixed tags within a container or 224 | at the root tag to multiple collections. 225 | 226 | ```xml 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | ``` 241 | 242 | ```java 243 | class WeatherReport { 244 | 245 | @Tag(path = "/WeatherReport/Locations", items = "Town") 246 | List towns; 247 | 248 | @Tag(path = "/WeatherReport/Locations", items = "City") 249 | List cities; 250 | } 251 | 252 | class Town { 253 | @Tag(path = "/WeatherReport/Locations/Town", attribute = "name") 254 | String name; 255 | } 256 | ``` 257 | 258 | ### Map types 259 | 260 | Maps can be deserialized either as a field or a top-level type. Consider the following XML document: 261 | ```xml 262 | 263 | 264 | 265 | 266 | 75 267 | °F 268 | 269 | 270 | 271 | ``` 272 | 273 | #### Option 1: Map a Specific Section 274 | 275 | You can map a specific section from the XML onto a custom field: 276 | 277 | ```java 278 | class WeatherData { 279 | @Tag(path = "/WeatherData/CurrentConditions") 280 | Map map; 281 | } 282 | ``` 283 | In this case, the map field will contain: 284 | ```java 285 | Map.of("Temperature", Map.of("Value", "75", "Unit", "°F")); 286 | ``` 287 | 288 | #### Option 2: Map the Whole Document 289 | 290 | Alternatively, you can map the entire document onto a Map of String Object 291 | ```java 292 | class WeatherData { 293 | @Tag(path = "/WeatherData") 294 | Map map; 295 | } 296 | ``` 297 | In this case, the map field will contain: 298 | ```java 299 | Map.of("CurrentConditions", 300 | Map.of("Temperature", Map.of("Value", "75", "Unit", "°F")))); 301 | ``` 302 | 303 | #### Option 3: Map to a Map 304 | ```java 305 | Map map = new XjxSerdes().read(document, new MapOf<>() {}); 306 | ``` 307 | In this case, the result of `read` will contain a Map of String Object 308 | ```java 309 | Map.of("CurrentConditions", 310 | Map.of("Temperature", Map.of("Value", "75", "Unit", "°F")))); 311 | ``` 312 | 313 | ## General serialization rules 314 | 315 | Fields annotated with `@Tag` are considered for serialization, while unannotated fields are ignored. 316 | ### Path Expressions 317 | Fields are serialized based on the path property specified in the @Tag annotation. 318 | The path property uses an XPath-like expression to determine the location of the field within the XML document. 319 | 320 | ```java 321 | class WeatherData { 322 | @Tag(path = "/WeatherData/Location/Country") 323 | private final String country; 324 | 325 | @Tag(path = "/WeatherData/Location/City/Name") 326 | private final String city; 327 | 328 | // Constructor and other methods are omitted for brevity 329 | } 330 | ``` 331 | 332 | Given that the above object is fully populated 333 | 334 | ```java 335 | var weatherData = new WeatherData("Belgium", "Ghent"); 336 | ``` 337 | The serialized result 338 | ```java 339 | new XjxSerdes().write(weatherData); 340 | ``` 341 | Would look like: 342 | ```xml 343 | 344 | 345 | Belgium 346 | 347 | Ghent 348 | 349 | 350 | 351 | ``` 352 | 353 | ## Null Fields 354 | 355 | Null fields are serialized as self-closing tags by default. 356 | If a field is null, the corresponding XML tag is included, but the tag content is empty. 357 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.1.1 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | # e.g. to debug Maven itself, use 32 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | # ---------------------------------------------------------------------------- 35 | 36 | if [ -z "$MAVEN_SKIP_RC" ] ; then 37 | 38 | if [ -f /usr/local/etc/mavenrc ] ; then 39 | . /usr/local/etc/mavenrc 40 | fi 41 | 42 | if [ -f /etc/mavenrc ] ; then 43 | . /etc/mavenrc 44 | fi 45 | 46 | if [ -f "$HOME/.mavenrc" ] ; then 47 | . "$HOME/.mavenrc" 48 | fi 49 | 50 | fi 51 | 52 | # OS specific support. $var _must_ be set to either true or false. 53 | cygwin=false; 54 | darwin=false; 55 | mingw=false 56 | case "`uname`" in 57 | CYGWIN*) cygwin=true ;; 58 | MINGW*) mingw=true;; 59 | Darwin*) darwin=true 60 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 61 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 62 | if [ -z "$JAVA_HOME" ]; then 63 | if [ -x "/usr/libexec/java_home" ]; then 64 | JAVA_HOME="`/usr/libexec/java_home`"; export JAVA_HOME 65 | else 66 | JAVA_HOME="/Library/Java/Home"; export JAVA_HOME 67 | fi 68 | fi 69 | ;; 70 | esac 71 | 72 | if [ -z "$JAVA_HOME" ] ; then 73 | if [ -r /etc/gentoo-release ] ; then 74 | JAVA_HOME=`java-config --jre-home` 75 | fi 76 | fi 77 | 78 | # For Cygwin, ensure paths are in UNIX format before anything is touched 79 | if $cygwin ; then 80 | [ -n "$JAVA_HOME" ] && 81 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 82 | [ -n "$CLASSPATH" ] && 83 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 84 | fi 85 | 86 | # For Mingw, ensure paths are in UNIX format before anything is touched 87 | if $mingw ; then 88 | [ -n "$JAVA_HOME" ] && 89 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 90 | fi 91 | 92 | if [ -z "$JAVA_HOME" ]; then 93 | javaExecutable="`which javac`" 94 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 95 | # readlink(1) is not available as standard on Solaris 10. 96 | readLink=`which readlink` 97 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 98 | if $darwin ; then 99 | javaHome="`dirname \"$javaExecutable\"`" 100 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 101 | else 102 | javaExecutable="`readlink -f \"$javaExecutable\"`" 103 | fi 104 | javaHome="`dirname \"$javaExecutable\"`" 105 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 106 | JAVA_HOME="$javaHome" 107 | export JAVA_HOME 108 | fi 109 | fi 110 | fi 111 | 112 | if [ -z "$JAVACMD" ] ; then 113 | if [ -n "$JAVA_HOME" ] ; then 114 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 115 | # IBM's JDK on AIX uses strange locations for the executables 116 | JAVACMD="$JAVA_HOME/jre/sh/java" 117 | else 118 | JAVACMD="$JAVA_HOME/bin/java" 119 | fi 120 | else 121 | JAVACMD="`\\unset -f command; \\command -v java`" 122 | fi 123 | fi 124 | 125 | if [ ! -x "$JAVACMD" ] ; then 126 | echo "Error: JAVA_HOME is not defined correctly." >&2 127 | echo " We cannot execute $JAVACMD" >&2 128 | exit 1 129 | fi 130 | 131 | if [ -z "$JAVA_HOME" ] ; then 132 | echo "Warning: JAVA_HOME environment variable is not set." 133 | fi 134 | 135 | # traverses directory structure from process work directory to filesystem root 136 | # first directory with .mvn subdirectory is considered project base directory 137 | find_maven_basedir() { 138 | if [ -z "$1" ] 139 | then 140 | echo "Path not specified to find_maven_basedir" 141 | return 1 142 | fi 143 | 144 | basedir="$1" 145 | wdir="$1" 146 | while [ "$wdir" != '/' ] ; do 147 | if [ -d "$wdir"/.mvn ] ; then 148 | basedir=$wdir 149 | break 150 | fi 151 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 152 | if [ -d "${wdir}" ]; then 153 | wdir=`cd "$wdir/.."; pwd` 154 | fi 155 | # end of workaround 156 | done 157 | printf '%s' "$(cd "$basedir"; pwd)" 158 | } 159 | 160 | # concatenates all lines of a file 161 | concat_lines() { 162 | if [ -f "$1" ]; then 163 | echo "$(tr -s '\n' ' ' < "$1")" 164 | fi 165 | } 166 | 167 | BASE_DIR=$(find_maven_basedir "$(dirname $0)") 168 | if [ -z "$BASE_DIR" ]; then 169 | exit 1; 170 | fi 171 | 172 | MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR 173 | if [ "$MVNW_VERBOSE" = true ]; then 174 | echo $MAVEN_PROJECTBASEDIR 175 | fi 176 | 177 | ########################################################################################## 178 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 179 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 180 | ########################################################################################## 181 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 182 | if [ "$MVNW_VERBOSE" = true ]; then 183 | echo "Found .mvn/wrapper/maven-wrapper.jar" 184 | fi 185 | else 186 | if [ "$MVNW_VERBOSE" = true ]; then 187 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 188 | fi 189 | if [ -n "$MVNW_REPOURL" ]; then 190 | wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 191 | else 192 | wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 193 | fi 194 | while IFS="=" read key value; do 195 | case "$key" in (wrapperUrl) wrapperUrl="$value"; break ;; 196 | esac 197 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 198 | if [ "$MVNW_VERBOSE" = true ]; then 199 | echo "Downloading from: $wrapperUrl" 200 | fi 201 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 202 | if $cygwin; then 203 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 204 | fi 205 | 206 | if command -v wget > /dev/null; then 207 | QUIET="--quiet" 208 | if [ "$MVNW_VERBOSE" = true ]; then 209 | echo "Found wget ... using wget" 210 | QUIET="" 211 | fi 212 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 213 | wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" 214 | else 215 | wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" 216 | fi 217 | [ $? -eq 0 ] || rm -f "$wrapperJarPath" 218 | elif command -v curl > /dev/null; then 219 | QUIET="--silent" 220 | if [ "$MVNW_VERBOSE" = true ]; then 221 | echo "Found curl ... using curl" 222 | QUIET="" 223 | fi 224 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 225 | curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L 226 | else 227 | curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L 228 | fi 229 | [ $? -eq 0 ] || rm -f "$wrapperJarPath" 230 | else 231 | if [ "$MVNW_VERBOSE" = true ]; then 232 | echo "Falling back to using Java to download" 233 | fi 234 | javaSource="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 235 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" 236 | # For Cygwin, switch paths to Windows format before running javac 237 | if $cygwin; then 238 | javaSource=`cygpath --path --windows "$javaSource"` 239 | javaClass=`cygpath --path --windows "$javaClass"` 240 | fi 241 | if [ -e "$javaSource" ]; then 242 | if [ ! -e "$javaClass" ]; then 243 | if [ "$MVNW_VERBOSE" = true ]; then 244 | echo " - Compiling MavenWrapperDownloader.java ..." 245 | fi 246 | # Compiling the Java class 247 | ("$JAVA_HOME/bin/javac" "$javaSource") 248 | fi 249 | if [ -e "$javaClass" ]; then 250 | # Running the downloader 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo " - Running MavenWrapperDownloader.java ..." 253 | fi 254 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 255 | fi 256 | fi 257 | fi 258 | fi 259 | ########################################################################################## 260 | # End of extension 261 | ########################################################################################## 262 | 263 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 264 | 265 | # For Cygwin, switch paths to Windows format before running java 266 | if $cygwin; then 267 | [ -n "$JAVA_HOME" ] && 268 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 269 | [ -n "$CLASSPATH" ] && 270 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 271 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 272 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 273 | fi 274 | 275 | # Provide a "standardized" way to retrieve the CLI args that will 276 | # work with both Windows and non-Windows executions. 277 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 278 | export MAVEN_CMD_LINE_ARGS 279 | 280 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 281 | 282 | exec "$JAVACMD" \ 283 | $MAVEN_OPTS \ 284 | $MAVEN_DEBUG_OPTS \ 285 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 286 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 287 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 288 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Apache Maven Wrapper startup batch script, version 3.1.1 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 28 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 29 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 30 | @REM e.g. to debug Maven itself, use 31 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 32 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 33 | @REM ---------------------------------------------------------------------------- 34 | 35 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 36 | @echo off 37 | @REM set title of command window 38 | title %0 39 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 40 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 41 | 42 | @REM set %HOME% to equivalent of $HOME 43 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 44 | 45 | @REM Execute a user defined script before this one 46 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 47 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 48 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 49 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 50 | :skipRcPre 51 | 52 | @setlocal 53 | 54 | set ERROR_CODE=0 55 | 56 | @REM To isolate internal variables from possible post scripts, we use another setlocal 57 | @setlocal 58 | 59 | @REM ==== START VALIDATION ==== 60 | if not "%JAVA_HOME%" == "" goto OkJHome 61 | 62 | echo. 63 | echo Error: JAVA_HOME not found in your environment. >&2 64 | echo Please set the JAVA_HOME variable in your environment to match the >&2 65 | echo location of your Java installation. >&2 66 | echo. 67 | goto error 68 | 69 | :OkJHome 70 | if exist "%JAVA_HOME%\bin\java.exe" goto init 71 | 72 | echo. 73 | echo Error: JAVA_HOME is set to an invalid directory. >&2 74 | echo JAVA_HOME = "%JAVA_HOME%" >&2 75 | echo Please set the JAVA_HOME variable in your environment to match the >&2 76 | echo location of your Java installation. >&2 77 | echo. 78 | goto error 79 | 80 | @REM ==== END VALIDATION ==== 81 | 82 | :init 83 | 84 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 85 | @REM Fallback to current working directory if not found. 86 | 87 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 88 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 89 | 90 | set EXEC_DIR=%CD% 91 | set WDIR=%EXEC_DIR% 92 | :findBaseDir 93 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 94 | cd .. 95 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 96 | set WDIR=%CD% 97 | goto findBaseDir 98 | 99 | :baseDirFound 100 | set MAVEN_PROJECTBASEDIR=%WDIR% 101 | cd "%EXEC_DIR%" 102 | goto endDetectBaseDir 103 | 104 | :baseDirNotFound 105 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 106 | cd "%EXEC_DIR%" 107 | 108 | :endDetectBaseDir 109 | 110 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 111 | 112 | @setlocal EnableExtensions EnableDelayedExpansion 113 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 114 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 115 | 116 | :endReadAdditionalConfig 117 | 118 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 123 | 124 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 125 | IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | if "%MVNW_VERBOSE%" == "true" ( 132 | echo Found %WRAPPER_JAR% 133 | ) 134 | ) else ( 135 | if not "%MVNW_REPOURL%" == "" ( 136 | SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 137 | ) 138 | if "%MVNW_VERBOSE%" == "true" ( 139 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 140 | echo Downloading from: %WRAPPER_URL% 141 | ) 142 | 143 | powershell -Command "&{"^ 144 | "$webclient = new-object System.Net.WebClient;"^ 145 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 146 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 147 | "}"^ 148 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ 149 | "}" 150 | if "%MVNW_VERBOSE%" == "true" ( 151 | echo Finished downloading %WRAPPER_JAR% 152 | ) 153 | ) 154 | @REM End of extension 155 | 156 | @REM Provide a "standardized" way to retrieve the CLI args that will 157 | @REM work with both Windows and non-Windows executions. 158 | set MAVEN_CMD_LINE_ARGS=%* 159 | 160 | %MAVEN_JAVA_EXE% ^ 161 | %JVM_CONFIG_MAVEN_PROPS% ^ 162 | %MAVEN_OPTS% ^ 163 | %MAVEN_DEBUG_OPTS% ^ 164 | -classpath %WRAPPER_JAR% ^ 165 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 166 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 167 | if ERRORLEVEL 1 goto error 168 | goto end 169 | 170 | :error 171 | set ERROR_CODE=1 172 | 173 | :end 174 | @endlocal & set ERROR_CODE=%ERROR_CODE% 175 | 176 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 177 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 178 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 179 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 180 | :skipRcPost 181 | 182 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 183 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 184 | 185 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 186 | 187 | cmd /C exit /B %ERROR_CODE% 188 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | io.jonasg 6 | xjx 7 | 0.3.4 8 | pom 9 | 10 | xjx-sax 11 | xjx-serdes 12 | 13 | 14 | xjx 15 | Lightweight XML library for Java 16 | https://github.com/jonasgeiregat/xjx 17 | 18 | 19 | 20 | Apache-2.0 21 | https://www.apache.org/licenses/LICENSE-2.0 22 | 23 | 24 | 25 | 26 | 27 | jonasgeiregat 28 | Jonas Geiregat 29 | jonas.grgt@gmail.com 30 | 31 | 32 | 33 | 34 | scm:git:https://github.com/jonasgeiregat/xjx.git 35 | scm:git:https://github.com/jonasgeiregat/xjx.git 36 | https://github.com/jonasgeiregat/xjx.git 37 | HEAD 38 | 39 | 40 | 41 | UTF-8 42 | 43 | 17 44 | 17 45 | 3.0.0 46 | 3.10.1 47 | 3.2.0 48 | 3.2.1 49 | 1.5.0 50 | 3.1.2 51 | 3.3.0 52 | 53 | 1.9.0 54 | 5.10.1 55 | 3.23.1 56 | 57 | 58 | 59 | 60 | 61 | org.junit 62 | junit-bom 63 | ${junit-bom.version} 64 | pom 65 | import 66 | 67 | 68 | org.assertj 69 | assertj-core 70 | ${assertj-core.version} 71 | test 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | org.apache.maven.plugins 81 | maven-deploy-plugin 82 | ${maven-deploy-plugin.version} 83 | 84 | 85 | org.apache.maven.plugins 86 | maven-compiler-plugin 87 | ${maven-compiler-plugin.version} 88 | 89 | 90 | org.apache.maven.plugins 91 | maven-javadoc-plugin 92 | ${maven-javadoc-plugin.version} 93 | 94 | 95 | org.apache.maven.plugins 96 | maven-source-plugin 97 | ${maven-source-plugin.version} 98 | 99 | 100 | org.apache.maven.plugins 101 | maven-surefire-plugin 102 | ${surefire-maven-plugin.version} 103 | 104 | 105 | org.codehaus.mojo 106 | flatten-maven-plugin 107 | ${flatten-maven-plugin.version} 108 | 109 | 110 | org.jreleaser 111 | jreleaser-maven-plugin 112 | ${jreleaser.version} 113 | 114 | 115 | 116 | 117 | 118 | 119 | org.codehaus.mojo 120 | flatten-maven-plugin 121 | 122 | 123 | clean 124 | clean 125 | 126 | clean 127 | 128 | 129 | 130 | flatten 131 | process-resources 132 | 133 | flatten 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | release 154 | 155 | 156 | 157 | org.jreleaser 158 | jreleaser-maven-plugin 159 | false 160 | 161 | 162 | 163 | Xjx - Lightweight XML Library for Java 164 | 165 | https://github.com/jonasgeiregat/xjx 166 | 167 | APACHE-2.0 168 | Jonas Geiregat 169 | 2023 Jonas Geiregat 170 | 171 | 172 | 173 | 174 | ALWAYS 175 | conventional-commits 176 | 177 | 178 | 179 | 180 | ALWAYS 181 | true 182 | 183 | 184 | 185 | 186 | 187 | ALWAYS 188 | https://s01.oss.sonatype.org/service/local 189 | https://s01.oss.sonatype.org/content/repositories/snapshots/ 190 | true 191 | true 192 | target/staging-deploy 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | org.apache.maven.plugins 202 | maven-javadoc-plugin 203 | 204 | 205 | attach-javadocs 206 | 207 | jar 208 | 209 | 210 | true 211 | 212 | 213 | 214 | 215 | 216 | org.apache.maven.plugins 217 | maven-source-plugin 218 | ${maven-source-plugin.version} 219 | 220 | 221 | attach-sources 222 | 223 | jar 224 | 225 | 226 | true 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | ossrh 239 | https://oss.sonatype.org/content/repositories/snapshots 240 | 241 | 242 | ossrh 243 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 244 | 245 | 246 | 247 | 248 | -------------------------------------------------------------------------------- /xjx-sax/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | io.jonasg 6 | xjx 7 | 0.3.4 8 | 9 | 10 | xjx-sax 11 | xjx-sax 12 | SAX based XML parser 13 | https://github.com/jonasgeiregat/xjx 14 | 15 | 16 | 17 17 | 17 18 | UTF-8 19 | 20 | 21 | 22 | 23 | org.junit.jupiter 24 | junit-jupiter 25 | test 26 | 27 | 28 | org.assertj 29 | assertj-core 30 | test 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /xjx-sax/src/main/java/io/jonasg/xjx/Attributes.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx; 2 | 3 | import java.util.LinkedHashMap; 4 | import java.util.Map; 5 | import java.util.StringJoiner; 6 | import java.util.stream.Stream; 7 | 8 | public class Attributes { 9 | private final Map attributes = new LinkedHashMap<>(); 10 | 11 | public Attributes(String... values) { 12 | int length = values.length; 13 | for (int i = 0; i < length - 1; i += 2) { 14 | String attributeName = values[i]; 15 | String attributeValue = values[i + 1]; 16 | attributes.put(attributeName, attributeValue); 17 | } 18 | 19 | if (length % 2 == 1) { 20 | String attributeName = values[length - 1]; 21 | attributes.put(attributeName, null); 22 | } 23 | } 24 | 25 | public static Attributes empty() { 26 | return new Attributes(); 27 | } 28 | 29 | public void add(String attributeName, String attributeValue) { 30 | attributes.put(attributeName, attributeValue); 31 | } 32 | 33 | public boolean isPresent() { 34 | return !attributes.isEmpty(); 35 | } 36 | 37 | public Stream stream() { 38 | return attributes.entrySet() 39 | .stream() 40 | .map(e -> new Attribute(e.getKey(), e.getValue())); 41 | } 42 | 43 | public boolean isEmpty() { 44 | return attributes.isEmpty(); 45 | } 46 | 47 | 48 | public record Attribute(String name, String value) {} 49 | 50 | @Override 51 | public boolean equals(Object o) { 52 | if (this == o) return true; 53 | if (o == null || getClass() != o.getClass()) return false; 54 | 55 | Attributes that = (Attributes) o; 56 | 57 | return attributes.equals(that.attributes); 58 | } 59 | 60 | @Override 61 | public int hashCode() { 62 | return attributes.hashCode(); 63 | } 64 | 65 | @Override 66 | public String toString() { 67 | return new StringJoiner(", ", Attributes.class.getSimpleName() + "[", "]") 68 | .add("attributes=" + attributes) 69 | .toString(); 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /xjx-sax/src/main/java/io/jonasg/xjx/BufferedPositionedReader.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.Reader; 6 | import java.util.Optional; 7 | 8 | public class BufferedPositionedReader implements PositionedReader { 9 | 10 | private final BufferedReader reader; 11 | 12 | private String currentLine; 13 | 14 | private int currentLinePos; 15 | 16 | private boolean hasMoreToRead = true; 17 | 18 | public BufferedPositionedReader(BufferedReader reader) { 19 | this.reader = reader; 20 | this.readNextLine(); 21 | } 22 | 23 | public BufferedPositionedReader(Reader reader) { 24 | this(new BufferedReader(reader)); 25 | } 26 | 27 | @Override 28 | public String currentLine() { 29 | if (currentLine != null && currentLinePos < currentLine.length()) { 30 | return currentLine.substring(currentLinePos); 31 | } 32 | return null; 33 | } 34 | 35 | private String readNextLine() { 36 | try { 37 | String readLine = reader.readLine(); 38 | if (readLine != null) { 39 | currentLine = readLine; 40 | currentLinePos = 0; 41 | } else { 42 | hasMoreToRead = false; 43 | currentLine = null; 44 | currentLinePos = 0; 45 | return null; 46 | } 47 | } catch (IOException e) { 48 | throw new RuntimeException(e); 49 | } 50 | return currentLine; 51 | } 52 | 53 | @Override 54 | public String peekLine() { 55 | if (currentLine == null) { 56 | return null; 57 | } 58 | if (currentLine.length() == currentLinePos) { 59 | readNextLine(); 60 | if (currentLine == null) { 61 | return null; 62 | } 63 | } 64 | return currentLine.substring(currentLinePos); 65 | } 66 | 67 | @Override 68 | public Character readOneChar() { 69 | if (currentLine == null) { 70 | return null; 71 | } 72 | if (currentLine.length() == currentLinePos) { 73 | readNextLine(); 74 | if (currentLine == null) { 75 | return null; 76 | } 77 | return '\n'; 78 | } 79 | return currentLine.charAt(currentLinePos++); 80 | } 81 | 82 | @Override 83 | public char peekOneChar() { 84 | if (currentLine.length() == currentLinePos) { 85 | return '\n'; 86 | } 87 | return currentLine.charAt(currentLinePos); 88 | 89 | } 90 | 91 | @Override 92 | public String readChars(int i) { 93 | currentLinePos += i; 94 | return currentLine.substring(currentLinePos - i, currentLinePos); 95 | } 96 | 97 | @Override 98 | public boolean hasMoreToRead() { 99 | if (currentLine.length() == currentLinePos) { 100 | currentLine = readNextLine(); 101 | } 102 | return hasMoreToRead; 103 | } 104 | 105 | @Override 106 | public Optional readUntil(String until) { 107 | if (currentLine == null) { 108 | return Optional.empty(); 109 | } 110 | var currentLineUntilPos = currentLine.substring(currentLinePos); 111 | var indexOfUntil = currentLineUntilPos.indexOf(until); 112 | if (indexOfUntil == -1) { 113 | this.currentLinePos = currentLine.length(); 114 | readNextLine(); 115 | return readUntil(until) 116 | .map(read -> currentLineUntilPos + "\n" + read); 117 | } else { 118 | var read = currentLineUntilPos.substring(0, indexOfUntil); 119 | this.currentLinePos = this.currentLinePos + indexOfUntil + until.length(); 120 | return Optional.of(read); 121 | } 122 | } 123 | 124 | @Override 125 | public Character getCurrentChar() { 126 | if (currentLine == null) { 127 | return null; 128 | } 129 | return currentLine.charAt(currentLinePos - 1); 130 | } 131 | 132 | @Override 133 | public void ltrim() { 134 | if (currentLine == null) { 135 | return; 136 | } 137 | if (currentLine.length() == currentLinePos) { 138 | readNextLine(); 139 | if (currentLine == null) { 140 | return; 141 | } 142 | } 143 | String replacedLine = removeLeadingWhitespace(currentLine()); 144 | currentLinePos += (currentLine().length() - replacedLine.length()); 145 | } 146 | 147 | public static String removeLeadingWhitespace(String input) { 148 | int length = input.length(); 149 | int startIndex = 0; 150 | while (startIndex < length && Character.isWhitespace(input.charAt(startIndex))) { 151 | startIndex++; 152 | } 153 | return input.substring(startIndex); 154 | } 155 | 156 | 157 | @Override 158 | public String toString() { 159 | return currentLine.substring(0, currentLinePos) + "|" + currentLine.substring(currentLinePos); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /xjx-sax/src/main/java/io/jonasg/xjx/EndTag.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx; 2 | 3 | public record EndTag(String namespace, String name) { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /xjx-sax/src/main/java/io/jonasg/xjx/PositionedReader.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx; 2 | 3 | import java.util.Optional; 4 | 5 | public interface PositionedReader { 6 | String currentLine(); 7 | 8 | String peekLine(); 9 | 10 | Character readOneChar(); 11 | 12 | char peekOneChar(); 13 | 14 | String readChars(int i); 15 | 16 | boolean hasMoreToRead(); 17 | 18 | Optional readUntil(String until); 19 | 20 | Character getCurrentChar(); 21 | 22 | void ltrim(); 23 | } 24 | -------------------------------------------------------------------------------- /xjx-sax/src/main/java/io/jonasg/xjx/StartTag.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx; 2 | 3 | public record StartTag(String name, String namespace, Attributes attributes) { 4 | 5 | public StartTag(String name) { 6 | this(name, null, Attributes.empty()); 7 | } 8 | 9 | public StartTag(String name, String namespace) { 10 | this(name, namespace, Attributes.empty()); 11 | } 12 | 13 | public StartTag(String name, Attributes attributes) { 14 | this(name, null, attributes); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /xjx-sax/src/main/java/io/jonasg/xjx/Token.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx; 2 | 3 | /** 4 | * Represents a single token in an XML document. 5 | * @param type the type of the token 6 | * @param value the value of the token or null if not relevant for that token type 7 | * @param the type of the value 8 | */ 9 | public record Token(Type type, T value) { 10 | 11 | public Token(Type type) { 12 | this(type, null); 13 | } 14 | 15 | public enum Type { 16 | START_TAG, 17 | END_TAG, 18 | START_COMMENT, 19 | SELF_CLOSING_TAG, 20 | CHARACTER_DATA, 21 | DOC_TYPE_DECLARATION, 22 | CLOSE_COMMENT 23 | } 24 | 25 | @Override 26 | public String toString() { 27 | return String.format("%s = %s", type, value); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /xjx-sax/src/main/java/io/jonasg/xjx/TokenEmitter.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx; 2 | 3 | public interface TokenEmitter { 4 | 5 | void emit(Token token); 6 | } 7 | -------------------------------------------------------------------------------- /xjx-sax/src/main/java/io/jonasg/xjx/Tokenizer.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx; 2 | 3 | import java.io.Reader; 4 | import java.util.Spliterator; 5 | import java.util.function.Consumer; 6 | import java.util.stream.Stream; 7 | import java.util.stream.StreamSupport; 8 | 9 | import io.jonasg.xjx.scanners.Scanner; 10 | import io.jonasg.xjx.scanners.WhiteSpaceScanner; 11 | 12 | /** 13 | * Tokenize an XML document into a stream of {@link Token}. 14 | */ 15 | public class Tokenizer { 16 | 17 | public Stream> tokenize(Reader reader) { 18 | return StreamSupport.stream(new TokenSpliterator(reader), false); 19 | } 20 | 21 | static class TokenSpliterator implements Spliterator> { 22 | 23 | private final BufferedPositionedReader reader; 24 | 25 | private Scanner scanner; 26 | 27 | public TokenSpliterator(Reader reader) { 28 | this.scanner = new WhiteSpaceScanner(); 29 | this.reader = new BufferedPositionedReader(reader); 30 | } 31 | 32 | @Override 33 | public boolean tryAdvance(Consumer> action) { 34 | if (scanner == null) { 35 | return false; 36 | } 37 | scanner = scanner.scan(reader, action::accept); 38 | return true; 39 | } 40 | 41 | @Override 42 | public Spliterator> trySplit() { 43 | return null; 44 | } 45 | 46 | @Override 47 | public long estimateSize() { 48 | return Long.MAX_VALUE; 49 | } 50 | 51 | @Override 52 | public int characteristics() { 53 | return ORDERED; 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /xjx-sax/src/main/java/io/jonasg/xjx/sax/Attribute.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.sax; 2 | 3 | public record Attribute(String name, String value) { 4 | } 5 | -------------------------------------------------------------------------------- /xjx-sax/src/main/java/io/jonasg/xjx/sax/SaxHandler.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.sax; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * A handler for SAX parsing. 7 | */ 8 | public interface SaxHandler { 9 | 10 | /** 11 | * Called when the document starts. 12 | */ 13 | void startDocument(); 14 | 15 | /** 16 | * Called when a start tag is encountered. 17 | * @param namespace the namespace of the tag or null when no namespace is present 18 | * @param name the name of the tag 19 | * @param attributes the attributes of the tag 20 | */ 21 | void startTag(String namespace, String name, List attributes); 22 | 23 | /** 24 | * Called when a end tag is encountered. 25 | * @param namespace the namespace of the tag or null when no namespace is present 26 | * @param name the name of the tag 27 | */ 28 | void endTag(String namespace, String name); 29 | 30 | /** 31 | * Called when character data is encountered. 32 | * @param data the character data 33 | */ 34 | void characters(String data); 35 | } 36 | -------------------------------------------------------------------------------- /xjx-sax/src/main/java/io/jonasg/xjx/sax/SaxParser.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.sax; 2 | 3 | import java.io.Reader; 4 | import java.util.List; 5 | 6 | import io.jonasg.xjx.EndTag; 7 | import io.jonasg.xjx.StartTag; 8 | import io.jonasg.xjx.Token; 9 | import io.jonasg.xjx.Tokenizer; 10 | 11 | public class SaxParser { 12 | 13 | private final Tokenizer tokenizer; 14 | 15 | public SaxParser() { 16 | this.tokenizer = new Tokenizer(); 17 | } 18 | 19 | public void parse(Reader reader, SaxHandler saxHandler) { 20 | saxHandler.startDocument(); 21 | tokenizer.tokenize(reader) 22 | .forEach(t -> handleToken(t, saxHandler)); 23 | } 24 | 25 | private void handleToken(Token token, SaxHandler saxHandler) { 26 | if (token.type().equals(Token.Type.START_TAG)) { 27 | var startTag = (StartTag)token.value(); 28 | var attributes = getAttributes(startTag); 29 | saxHandler.startTag(startTag.namespace(), startTag.name(), attributes); 30 | } else if (token.type().equals(Token.Type.SELF_CLOSING_TAG)) { 31 | var startTag = (StartTag)token.value(); 32 | var attributes = getAttributes(startTag); 33 | saxHandler.startTag(startTag.namespace(), startTag.name(), attributes); 34 | saxHandler.endTag(startTag.namespace(), startTag.name()); 35 | } else if (token.type().equals(Token.Type.END_TAG)) { 36 | var endTag = (EndTag)token.value(); 37 | saxHandler.endTag(endTag.namespace(), endTag.name()); 38 | } else if (token.type().equals(Token.Type.CHARACTER_DATA)) { 39 | saxHandler.characters((String)token.value()); 40 | } 41 | } 42 | 43 | private List getAttributes(StartTag startTagValue) { 44 | return startTagValue.attributes().stream() 45 | .map(a -> new Attribute(a.name(), a.value())) 46 | .toList(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /xjx-sax/src/main/java/io/jonasg/xjx/scanners/CDATAScanner.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.scanners; 2 | 3 | import io.jonasg.xjx.PositionedReader; 4 | import io.jonasg.xjx.Token; 5 | import io.jonasg.xjx.TokenEmitter; 6 | 7 | class CDATAScanner implements Scanner { 8 | 9 | private static final int CDATA_OPENING_TAG_LENGTH = 9; 10 | 11 | @Override 12 | public Scanner scan(PositionedReader reader, TokenEmitter tokenEmitter) { 13 | reader.readChars(CDATA_OPENING_TAG_LENGTH); 14 | var characters = reader.readUntil("]]>") 15 | .orElseThrow(() -> new XmlParsingException("CDATA tag found without closing ]]")); 16 | tokenEmitter.emit(new Token<>(Token.Type.CHARACTER_DATA, characters)); 17 | return Scanner.nextScanner(reader); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /xjx-sax/src/main/java/io/jonasg/xjx/scanners/CharacterScanner.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.scanners; 2 | 3 | import io.jonasg.xjx.PositionedReader; 4 | import io.jonasg.xjx.Token; 5 | import io.jonasg.xjx.TokenEmitter; 6 | 7 | class CharacterScanner implements Scanner { 8 | @Override 9 | public Scanner scan(PositionedReader reader, TokenEmitter tokenEmitter) { 10 | char character = reader.peekOneChar(); 11 | var builder = new StringBuilder(); 12 | while (character != '<') { 13 | builder.append(reader.readOneChar()); 14 | character = reader.peekOneChar(); 15 | } 16 | tokenEmitter.emit(new Token<>(Token.Type.CHARACTER_DATA, builder.toString())); 17 | return Scanner.nextScanner(reader); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /xjx-sax/src/main/java/io/jonasg/xjx/scanners/CommentBodyScanner.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.scanners; 2 | 3 | import static io.jonasg.xjx.Token.Type.CLOSE_COMMENT; 4 | import io.jonasg.xjx.PositionedReader; 5 | import io.jonasg.xjx.Token; 6 | import io.jonasg.xjx.TokenEmitter; 7 | 8 | class CommentBodyScanner implements Scanner { 9 | @Override 10 | public Scanner scan(PositionedReader reader, TokenEmitter tokenEmitter) { 11 | var commentBody = reader.readUntil("-->") 12 | .orElseThrow(() -> new XmlParsingException("Comment tag found without ending -->")); 13 | tokenEmitter.emit(new Token<>(CLOSE_COMMENT, commentBody)); 14 | return Scanner.nextScanner(reader); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /xjx-sax/src/main/java/io/jonasg/xjx/scanners/DocumentTypeDeclarationScanner.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.scanners; 2 | 3 | import io.jonasg.xjx.PositionedReader; 4 | import io.jonasg.xjx.Token; 5 | import io.jonasg.xjx.TokenEmitter; 6 | 7 | class DocumentTypeDeclarationScanner implements Scanner { 8 | @Override 9 | public Scanner scan(PositionedReader reader, TokenEmitter tokenEmitter) { 10 | var read = reader.readChars(5); 11 | if (!"").orElseThrow(() -> new XmlParsingException("Document type declaration never closed")); 15 | tokenEmitter.emit(new Token<>(Token.Type.DOC_TYPE_DECLARATION)); 16 | return Scanner.nextScanner(reader); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /xjx-sax/src/main/java/io/jonasg/xjx/scanners/EndTagScanner.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.scanners; 2 | 3 | import java.util.Objects; 4 | 5 | import io.jonasg.xjx.EndTag; 6 | import io.jonasg.xjx.PositionedReader; 7 | import io.jonasg.xjx.Token; 8 | import io.jonasg.xjx.TokenEmitter; 9 | 10 | class EndTagScanner implements Scanner { 11 | 12 | @Override 13 | public Scanner scan(PositionedReader reader, TokenEmitter tokenEmitter) { 14 | reader.ltrim(); 15 | String opening = reader.readChars(2); 16 | if (!Objects.equals(opening, "') { 21 | builder.append(reader.readOneChar()); 22 | } 23 | reader.readOneChar(); 24 | var name = builder.toString(); 25 | String actualName = name; 26 | String namespace = null; 27 | if (name.contains(":")) { 28 | var splitName = name.split(":"); 29 | namespace = splitName[0]; 30 | actualName = splitName[1]; 31 | } 32 | tokenEmitter.emit(new Token<>(Token.Type.END_TAG, new EndTag(namespace, actualName))); 33 | return Scanner.nextScanner(reader); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /xjx-sax/src/main/java/io/jonasg/xjx/scanners/Scanner.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.scanners; 2 | 3 | import io.jonasg.xjx.PositionedReader; 4 | import io.jonasg.xjx.TokenEmitter; 5 | 6 | @FunctionalInterface 7 | public interface Scanner { 8 | 9 | Scanner START_COMMENT_SCANNER = new StartCommentScanner(); 10 | Scanner END_TAG_SCANNER = new EndTagScanner(); 11 | Scanner START_TAG_SCANNER = new StartTagScanner(); 12 | Scanner CDATA_SCANNER = new CDATAScanner(); 13 | Scanner CHARACTER_SCANNER = new CharacterScanner(); 14 | Scanner DOCUMENT_START_SCANNER = new DocumentTypeDeclarationScanner(); 15 | Scanner WHITE_SPACE_SCANNER = new WhiteSpaceScanner(); 16 | 17 | static Scanner nextScanner(PositionedReader reader) { 18 | var peekedLine = reader.peekLine(); 19 | if (peekedLine == null) { 20 | return null; 21 | } 22 | if (!peekedLine.isEmpty()) { 23 | return scannerForLine(peekedLine); 24 | } 25 | return WHITE_SPACE_SCANNER; 26 | } 27 | 28 | private static Scanner scannerForLine(String peekedLine) { 29 | var cleanedLine = removeLeadingWhitespace(peekedLine); 30 | if (cleanedLine.startsWith("")), tokens::add); 23 | 24 | // then 25 | Assertions.assertThat(tokens).containsExactly(new Token<>(Token.Type.CLOSE_COMMENT, "bla")); 26 | } 27 | 28 | @Test 29 | void shouldEmit_closeCommentToken_forMultiLineComment() { 30 | // given 31 | var scanner = new CommentBodyScanner(); 32 | List> tokens = new ArrayList<>(); 33 | 34 | // when 35 | scanner.scan(new BufferedPositionedReader(new StringReader("bla\nbla\n\tbla-->")), tokens::add); 36 | 37 | // then 38 | Assertions.assertThat(tokens).containsExactly(new Token<>(Token.Type.CLOSE_COMMENT, "bla\nbla\n\tbla")); 39 | } 40 | 41 | @Test 42 | void shouldThrowException_whenNoCommentCloseTagIsPresent() { 43 | // given 44 | var scanner = new CommentBodyScanner(); 45 | 46 | // when 47 | ThrowableAssert.ThrowingCallable act = () -> scanner.scan(new BufferedPositionedReader(new StringReader("bla\nbla\n\tbla")), a -> {}); 48 | 49 | // then 50 | Assertions.assertThatThrownBy(act) 51 | .isInstanceOf(XmlParsingException.class) 52 | .hasMessage("Comment tag found without ending -->"); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /xjx-sax/src/test/java/io/jonasg/xjx/scanners/DocumentTypeDeclarationScannerTest.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.scanners; 2 | 3 | import java.io.StringReader; 4 | import java.util.ArrayList; 5 | 6 | import org.assertj.core.api.Assertions; 7 | import org.assertj.core.api.ThrowableAssert; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import io.jonasg.xjx.BufferedPositionedReader; 11 | import io.jonasg.xjx.Token; 12 | 13 | class DocumentTypeDeclarationScannerTest { 14 | 15 | @Test 16 | void shouldReadDocumentTypeDeclaration() { 17 | // given 18 | var scanner = new DocumentTypeDeclarationScanner(); 19 | var tokens = new ArrayList>(); 20 | 21 | // when 22 | scanner.scan(new BufferedPositionedReader(new StringReader("")), tokens::add); 23 | 24 | // then 25 | Assertions.assertThat(tokens).containsExactly(new Token<>(Token.Type.DOC_TYPE_DECLARATION)); 26 | } 27 | 28 | @Test 29 | void shouldThrowException_whenDeclaration_isNeverClosed() { 30 | // given 31 | var scanner = new DocumentTypeDeclarationScanner(); 32 | 33 | // when 34 | ThrowableAssert.ThrowingCallable act = () -> scanner.scan(new BufferedPositionedReader(new StringReader(" {}); 35 | 36 | // then 37 | Assertions.assertThatThrownBy(act) 38 | .isInstanceOf(XmlParsingException.class) 39 | .hasMessage("Document type declaration never closed"); 40 | } 41 | 42 | @Test 43 | void shouldThrowException_whenDeclaration_isNotOpened() { 44 | // given 45 | var scanner = new DocumentTypeDeclarationScanner(); 46 | 47 | // when 48 | ThrowableAssert.ThrowingCallable act = () -> scanner.scan(new BufferedPositionedReader(new StringReader("version=\"1.0\"?>")), t -> {}); 49 | 50 | // then 51 | Assertions.assertThatThrownBy(act) 52 | .isInstanceOf(XmlParsingException.class) 53 | .hasMessage("Document type declaration not declared correctly"); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /xjx-sax/src/test/java/io/jonasg/xjx/scanners/EndTagScannerTest.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.scanners; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 5 | 6 | import java.io.StringReader; 7 | import java.util.ArrayList; 8 | 9 | import org.assertj.core.api.ThrowableAssert; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import io.jonasg.xjx.BufferedPositionedReader; 13 | import io.jonasg.xjx.EndTag; 14 | import io.jonasg.xjx.Token; 15 | 16 | class EndTagScannerTest { 17 | 18 | 19 | @Test 20 | void shouldFail_whenInputDoesNotStartWithALessThenSignFollowedByASlash() { 21 | // given 22 | var endTagAction = new EndTagScanner(); 23 | 24 | // when 25 | ThrowableAssert.ThrowingCallable act = () -> endTagAction.scan(new BufferedPositionedReader(new StringReader("home")), t -> {}); 26 | 27 | // then 28 | assertThatThrownBy(act) 29 | .isInstanceOf(XmlParsingException.class) 30 | .hasMessage("End tag does not start with '"); 31 | } 32 | 33 | @Test 34 | void shouldEmit_endTag() { 35 | // given 36 | var endTagAction = new EndTagScanner(); 37 | var tokens = new ArrayList>(); 38 | 39 | // when 40 | endTagAction.scan(new BufferedPositionedReader(new StringReader("")), tokens::add); 41 | 42 | // then 43 | assertThat(tokens).containsExactly(new Token<>(Token.Type.END_TAG, new EndTag(null, "a"))); 44 | } 45 | 46 | 47 | @Test 48 | void shouldEmit_endTagWithNamespace() { 49 | // given 50 | var endTagAction = new EndTagScanner(); 51 | var tokens = new ArrayList>(); 52 | 53 | // when 54 | endTagAction.scan(new BufferedPositionedReader(new StringReader("")), tokens::add); 55 | 56 | // then 57 | assertThat(tokens).containsExactly(new Token<>(Token.Type.END_TAG, new EndTag("a", "b"))); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /xjx-sax/src/test/java/io/jonasg/xjx/scanners/StartTagScannerTest.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.scanners; 2 | 3 | import java.io.StringReader; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | import org.assertj.core.api.Assertions; 8 | import org.assertj.core.api.ThrowableAssert; 9 | import org.junit.jupiter.api.Test; 10 | 11 | import io.jonasg.xjx.Attributes; 12 | import io.jonasg.xjx.BufferedPositionedReader; 13 | import io.jonasg.xjx.StartTag; 14 | import io.jonasg.xjx.Token; 15 | 16 | class StartTagScannerTest { 17 | 18 | @Test 19 | void shouldFail_whenInputDoesNotStartWithALessThenSign() { 20 | // given 21 | var startTagAction = new StartTagScanner(); 22 | List> tokens = new ArrayList<>(); 23 | 24 | // when 25 | ThrowableAssert.ThrowingCallable act = () -> startTagAction.scan(new BufferedPositionedReader(new StringReader("a>home")), tokens::add); 26 | 27 | // then 28 | Assertions.assertThatThrownBy(act) 29 | .isInstanceOf(XmlParsingException.class) 30 | .hasMessage("Start tag missing < in: 'a>home'"); 31 | } 32 | 33 | @Test 34 | void shouldEmit_startTag() { 35 | // given 36 | var startTagAction = new StartTagScanner(); 37 | List> tokens = new ArrayList<>(); 38 | 39 | // when 40 | startTagAction.scan(new BufferedPositionedReader(new StringReader("home")), tokens::add); 41 | 42 | // then 43 | Assertions.assertThat(tokens).containsExactly(new Token<>(Token.Type.START_TAG, new StartTag("a"))); 44 | } 45 | 46 | 47 | @Test 48 | void shouldEmit_namespacedStartTag() { 49 | // given 50 | var startTagAction = new StartTagScanner(); 51 | List> tokens = new ArrayList<>(); 52 | 53 | // when 54 | startTagAction.scan(new BufferedPositionedReader(new StringReader("home")), tokens::add); 55 | 56 | // then 57 | Assertions.assertThat(tokens).containsExactly(new Token<>(Token.Type.START_TAG, new StartTag("a", "bk"))); 58 | } 59 | 60 | @Test 61 | void shouldEmit_startTag_spreadOverMultipleLines() { 62 | // given 63 | var startTagAction = new StartTagScanner(); 64 | List> tokens = new ArrayList<>(); 65 | 66 | // when 67 | startTagAction.scan(new BufferedPositionedReader(new StringReader("home")), tokens::add); 68 | 69 | // then 70 | Assertions.assertThat(tokens).containsExactly(new Token<>(Token.Type.START_TAG, new StartTag("a"))); 71 | } 72 | 73 | @Test 74 | void shouldEmit_singleNamespaceDeclarationAttribute() { 75 | // given 76 | var startTagAction = new StartTagScanner(); 77 | List> tokens = new ArrayList<>(); 78 | 79 | // when 80 | startTagAction.scan(new BufferedPositionedReader(new StringReader("home")), tokens::add); 81 | 82 | // then 83 | Assertions.assertThat(tokens) 84 | .containsExactly(new Token<>(Token.Type.START_TAG, new StartTag("a", new Attributes("xmlns:bk", "urn:example.com")))); 85 | } 86 | 87 | @Test 88 | void shouldEmit_singleAttribute_onOneLine() { 89 | // given 90 | var startTagAction = new StartTagScanner(); 91 | List> tokens = new ArrayList<>(); 92 | 93 | // when 94 | startTagAction.scan(new BufferedPositionedReader(new StringReader("home")), tokens::add); 95 | 96 | // then 97 | Assertions.assertThat(tokens) 98 | .containsExactly(new Token<>(Token.Type.START_TAG, new StartTag("a", new Attributes("href", "home")))); 99 | } 100 | 101 | @Test 102 | void shouldEmit_singleAttribute_whereFirstAttributeIsPrefixedWithNewLinesAndTab() { 103 | // given 104 | var startTagAction = new StartTagScanner(); 105 | List> tokens = new ArrayList<>(); 106 | 107 | // when 108 | startTagAction.scan(new BufferedPositionedReader(new StringReader("home")), tokens::add); 109 | 110 | // then 111 | Assertions.assertThat(tokens) 112 | .containsExactly(new Token<>(Token.Type.START_TAG, new StartTag("a", new Attributes("href", "home")))); 113 | } 114 | 115 | @Test 116 | void shouldEmit_multipleAttributes_onOneLine() { 117 | // given 118 | var startTagAction = new StartTagScanner(); 119 | List> tokens = new ArrayList<>(); 120 | 121 | // when 122 | startTagAction.scan(new BufferedPositionedReader(new StringReader("home")), tokens::add); 123 | 124 | // then 125 | Assertions.assertThat(tokens) 126 | .containsExactly(new Token<>(Token.Type.START_TAG, new StartTag("a", new Attributes("href", "home", "target", "_blank")))); 127 | } 128 | 129 | @Test 130 | void shouldEmit_multipleAttributes_whereAllAttributeArePrefixedWithNewLinesAndTabs() { 131 | // given 132 | var startTagAction = new StartTagScanner(); 133 | List> tokens = new ArrayList<>(); 134 | 135 | // when 136 | startTagAction.scan(new BufferedPositionedReader(new StringReader("home")), tokens::add); 137 | 138 | // then 139 | Assertions.assertThat(tokens) 140 | .containsExactly(new Token<>(Token.Type.START_TAG, new StartTag("a", new Attributes("href", "home", "target", "_blank")))); 141 | } 142 | 143 | @Test 144 | void shouldEmit_selfClosingTag() { 145 | // given 146 | var startTagAction = new StartTagScanner(); 147 | List> tokens = new ArrayList<>(); 148 | 149 | // when 150 | startTagAction.scan(new BufferedPositionedReader(new StringReader("c")), tokens::add); 151 | 152 | // then 153 | Assertions.assertThat(tokens).containsExactly(new Token<>(Token.Type.SELF_CLOSING_TAG, new StartTag("a"))); 154 | } 155 | 156 | 157 | @Test 158 | void shouldEmit_selfClosingTagWithNamespace() { 159 | // given 160 | var startTagAction = new StartTagScanner(); 161 | List> tokens = new ArrayList<>(); 162 | 163 | // when 164 | startTagAction.scan(new BufferedPositionedReader(new StringReader("c")), tokens::add); 165 | 166 | // then 167 | Assertions.assertThat(tokens).containsExactly(new Token<>(Token.Type.SELF_CLOSING_TAG, new StartTag("foo", "a"))); 168 | } 169 | 170 | @Test 171 | void shouldEmit_selfClosingTag_withAttributes() { 172 | // given 173 | var startTagAction = new StartTagScanner(); 174 | List> tokens = new ArrayList<>(); 175 | 176 | // when 177 | startTagAction.scan(new BufferedPositionedReader(new StringReader("")), tokens::add); 178 | 179 | // then 180 | Assertions.assertThat(tokens) 181 | .containsExactly(new Token<>(Token.Type.SELF_CLOSING_TAG, new StartTag("a", new Attributes("href", "home", "target", "_blank")))); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /xjx-serdes/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | io.jonasg 6 | xjx 7 | 0.3.4 8 | 9 | 10 | xjx-serdes 11 | xjx-serdes 12 | XML Serializer and Deserializer library for Java 13 | https://github.com/jonasgeiregat/xjx 14 | 15 | 16 | 17 17 | 17 18 | UTF-8 19 | 20 | 21 | 22 | 23 | io.jonasg 24 | xjx-sax 25 | ${project.version} 26 | 27 | 28 | org.junit.jupiter 29 | junit-jupiter 30 | test 31 | 32 | 33 | org.assertj 34 | assertj-core 35 | test 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/Path.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes; 2 | 3 | import java.util.Arrays; 4 | import java.util.Iterator; 5 | import java.util.LinkedList; 6 | import java.util.Objects; 7 | 8 | public class Path implements Iterable
{ 9 | 10 | private final LinkedList sections = new LinkedList<>(); 11 | 12 | private String attribute; 13 | 14 | private Path(String... paths) { 15 | Arrays.stream(paths) 16 | .filter(p -> !p.isEmpty()) 17 | .forEach(sections::add); 18 | } 19 | 20 | private Path(LinkedList sections, String section) { 21 | this.sections.addAll(sections); 22 | this.sections.add(section); 23 | } 24 | 25 | public Path(LinkedList newSections) { 26 | this.sections.addAll(newSections); 27 | } 28 | 29 | public static Path of(String... paths) { 30 | return new Path(paths); 31 | } 32 | 33 | public static Path parse(String path) { 34 | return Path.of(path.split("/")); 35 | } 36 | 37 | public Path appendAttribute(String attribute) { 38 | var copiedSections = new LinkedList<>(this.sections); 39 | var path = new Path(copiedSections); 40 | path.attribute = attribute; 41 | return path; 42 | } 43 | 44 | public Path append(String section) { 45 | return new Path(sections, section); 46 | } 47 | 48 | public Path append(Path path) { 49 | var copiedSections = new LinkedList<>(this.sections); 50 | copiedSections.addAll(path.sections); 51 | return new Path(copiedSections); 52 | } 53 | 54 | public Path pop() { 55 | if (!sections.isEmpty()) { 56 | LinkedList newSections = new LinkedList<>(sections); 57 | newSections.removeLast(); 58 | return new Path(newSections); 59 | } else { 60 | return new Path(); 61 | } 62 | } 63 | 64 | public String getRoot() { 65 | return sections.getFirst(); 66 | } 67 | 68 | public int size() { 69 | return sections.size(); 70 | } 71 | 72 | public boolean isRoot() { 73 | return sections.size() == 1; 74 | } 75 | 76 | public Section getSection(int position) { 77 | return new Section(this.sections.get(position), position == this.sections.size() - 1); 78 | } 79 | 80 | @Override 81 | public Iterator
iterator() { 82 | int size = sections.size(); 83 | return sections.stream() 84 | .map(s -> new Section(s, sections.indexOf(s) == size - 1)) 85 | .iterator(); 86 | } 87 | 88 | @Override 89 | public boolean equals(Object o) { 90 | if (this == o) return true; 91 | if (o == null || getClass() != o.getClass()) return false; 92 | 93 | Path path = (Path) o; 94 | 95 | if (!sections.equals(path.sections)) return false; 96 | return Objects.equals(attribute, path.attribute); 97 | } 98 | 99 | @Override 100 | public int hashCode() { 101 | int result = sections.hashCode(); 102 | result = 31 * result + Objects.hashCode(attribute); 103 | return result; 104 | } 105 | 106 | @Override 107 | public String toString() { 108 | return "/" + String.join("/", sections) + (attribute == null ? "" : "[" + attribute + "]"); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/Section.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes; 2 | 3 | public record Section(String name, boolean isLeaf) { 4 | } 5 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/Tag.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * The {@code Tag} annotation is used to mark a field for XML serialization and deserialization. 10 | * It provides information about the XML path and optional attributes to be used during serialization and deserialization. 11 | * 12 | *

Example XML document:

13 | *
{@code
14 |  * 
15 |  *     Product 1
16 |  *     Product 2
17 |  *     Product 3
18 |  * 
19 |  * }
20 | * 21 | *

Example Usage:

22 | *
{@code
23 |  * @Tag(path = "/Products", items = "Name")
24 |  * List productNames;
25 |  * }
26 | * In this example, the {@code List} field 'productNames' will be serialized to and deserialized from the XML path "/Products/Name". 27 | * 28 | *

Example XML for Serialization:

29 | *
{@code
30 |  * 
31 |  *     Product 1
32 |  *     Product 2
33 |  *     Product 3
34 |  * 
35 |  * }
36 | * In this example, when the {@code List} field 'productNames' is serialized, the generated XML will look like the above representation. 37 | * 38 | *

Annotation Usage:

39 | *
    40 | *
  • {@code path}: Specifies the Path expression indicating the location of the XML data for serialization and deserialization.
  • 41 | *
  • {@code attribute}: Specifies the name of an XML attribute to be used during serialization and deserialization (optional).
  • 42 | *
  • {@code items}: Specifies additional information for serializing and deserializing items within a collection (optional).
  • 43 | *
44 | */ 45 | @Retention(RetentionPolicy.RUNTIME) 46 | @Target({ElementType.TYPE, ElementType.FIELD, ElementType.RECORD_COMPONENT}) 47 | public @interface Tag { 48 | /** 49 | * Specifies the Path expression indicating the location of the XML data for serialization and deserialization. 50 | * 51 | * @return The Path expression representing the location of the XML data. 52 | */ 53 | String path(); 54 | 55 | /** 56 | * Specifies the name of an XML attribute to be used during serialization and deserialization (optional). 57 | * 58 | * @return The name of the XML attribute. 59 | */ 60 | String attribute() default ""; 61 | 62 | /** 63 | * Specifies additional information for serializing and deserializing items within a collection (optional). 64 | * 65 | * @return Additional information for serializing and deserializing items. 66 | */ 67 | String items() default ""; 68 | } 69 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/TypeMappers.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes; 2 | 3 | import io.jonasg.xjx.serdes.deserialize.XjxDeserializationException; 4 | import io.jonasg.xjx.serdes.deserialize.config.XjxConfiguration; 5 | 6 | import java.math.BigDecimal; 7 | import java.time.LocalDate; 8 | import java.time.LocalDateTime; 9 | import java.time.ZonedDateTime; 10 | import java.util.HashSet; 11 | import java.util.List; 12 | import java.util.Set; 13 | import java.util.function.Function; 14 | 15 | public final class TypeMappers { 16 | 17 | static List> DOUBLE_TYPES = List.of(double.class, Double.class); 18 | static List> LONG_TYPES = List.of(long.class, Long.class); 19 | static List> INTEGER_TYPES = List.of(int.class, Integer.class); 20 | static List> CHAR_TYPES = List.of(char.class, Character.class); 21 | static List> BOOLEAN_TYPES = List.of(boolean.class, Boolean.class); 22 | 23 | public static Set> TYPES; 24 | 25 | static { 26 | TYPES = new HashSet<>(); 27 | TYPES.addAll(INTEGER_TYPES); 28 | TYPES.addAll(DOUBLE_TYPES); 29 | TYPES.addAll(LONG_TYPES); 30 | TYPES.addAll(CHAR_TYPES); 31 | TYPES.addAll(BOOLEAN_TYPES); 32 | TYPES.add(String.class); 33 | TYPES.add(LocalDate.class); 34 | } 35 | 36 | public static Function forType(Class type, XjxConfiguration configuration) { 37 | Function mapper = Function.identity(); 38 | if (type.equals(String.class)) { 39 | mapper = String::valueOf; 40 | } 41 | if (INTEGER_TYPES.contains(type)) { 42 | mapper = value -> Integer.parseInt(String.valueOf(value)); 43 | } 44 | if (LONG_TYPES.contains(type)) { 45 | mapper = value -> Long.parseLong(String.valueOf(value)); 46 | } 47 | if (type.equals(BigDecimal.class)) { 48 | mapper = value -> new BigDecimal(String.valueOf(value)); 49 | } 50 | if (DOUBLE_TYPES.contains(type)) { 51 | mapper = value -> Double.valueOf(String.valueOf(value)); 52 | } 53 | if (CHAR_TYPES.contains(type)) { 54 | mapper = value -> String.valueOf(value).charAt(0); 55 | } 56 | if (BOOLEAN_TYPES.contains(type)) { 57 | mapper = value -> { 58 | String lowered = String.valueOf(value).toLowerCase(); 59 | if (lowered.equals("true") || lowered.equals("yes") || lowered.equals("1")) { 60 | return true; 61 | } 62 | return false; 63 | }; 64 | } 65 | if (type.equals(LocalDate.class)) { 66 | mapper = value -> LocalDate.parse(String.valueOf(value)); 67 | } 68 | if (type.equals(LocalDateTime.class)) { 69 | mapper = value -> LocalDateTime.parse(String.valueOf(value)); 70 | } 71 | if (type.equals(ZonedDateTime.class)) { 72 | mapper = value -> ZonedDateTime.parse(String.valueOf(value)); 73 | } 74 | if (type.isEnum()) { 75 | mapper = value -> { 76 | Object enumValue = toEnum(type, String.valueOf(value)); 77 | if (enumValue == null && configuration.failOnUnknownEnumValue()) { 78 | throw new XjxDeserializationException("Cannot map value '" + value + "' to enum " + type.getSimpleName()); 79 | } 80 | return enumValue; 81 | }; 82 | } 83 | return mapper; 84 | } 85 | 86 | @SuppressWarnings("unchecked") 87 | private static > T toEnum(Class type, String value) { 88 | try { 89 | T[] enumConstants = (T[]) type.getEnumConstants(); 90 | for (T constant : enumConstants) { 91 | if (value.equals(constant.name())) { 92 | return constant; 93 | } 94 | } 95 | } catch (Exception e) { 96 | throw new RuntimeException(e); 97 | } 98 | return null; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/XjxSerdes.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes; 2 | 3 | import java.io.Reader; 4 | import java.io.StringReader; 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.function.Consumer; 8 | 9 | import io.jonasg.xjx.sax.SaxParser; 10 | import io.jonasg.xjx.serdes.deserialize.config.ConfigurationBuilder; 11 | import io.jonasg.xjx.serdes.deserialize.MapOf; 12 | import io.jonasg.xjx.serdes.deserialize.MapRootSaxHandler; 13 | import io.jonasg.xjx.serdes.deserialize.PathBasedSaxHandler; 14 | import io.jonasg.xjx.serdes.deserialize.PathWriterIndexFactory; 15 | import io.jonasg.xjx.serdes.deserialize.XjxDeserializationException; 16 | import io.jonasg.xjx.serdes.deserialize.config.XjxConfiguration; 17 | import io.jonasg.xjx.serdes.serialize.XmlNodeStructureFactory; 18 | import io.jonasg.xjx.serdes.serialize.XmlStringBuilder; 19 | 20 | /** 21 | * XjxSerdes provides functionality for serializing and deserializing objects to and from XML. 22 | */ 23 | public class XjxSerdes { 24 | 25 | private final SaxParser saxParser; 26 | 27 | private final PathWriterIndexFactory pathWriterIndexFactory; 28 | 29 | private final XmlNodeStructureFactory xmlNodeStructureFactory = new XmlNodeStructureFactory(); 30 | 31 | private final XmlStringBuilder xmlStringBuilder; 32 | 33 | private final XjxConfiguration configuration; 34 | 35 | private XjxSerdes(SaxParser saxParser, 36 | XmlStringBuilder xmlStringBuilder, 37 | Consumer configurationBuilder) { 38 | this.configuration = new XjxConfiguration(); 39 | configurationBuilder.accept(new ConfigurationBuilder(configuration)); 40 | this.saxParser = saxParser; 41 | this.pathWriterIndexFactory = new PathWriterIndexFactory(configuration); 42 | this.xmlStringBuilder = xmlStringBuilder; 43 | } 44 | 45 | /** 46 | * Constructs an XjxSerdes instance with default configurations. 47 | */ 48 | public XjxSerdes() { 49 | this(new SaxParser(), new XmlStringBuilder(), (builder) -> {}); 50 | } 51 | 52 | /** 53 | * Constructs an XjxSerdes instance with custom configurations. 54 | * @param configurationBuilder The configuration builder to configure the XjxSerdes instance. 55 | */ 56 | public XjxSerdes(Consumer configurationBuilder) { 57 | this(new SaxParser(), new XmlStringBuilder(), configurationBuilder); 58 | } 59 | 60 | /** 61 | * Reads XML data and deserializes it into an object of the specified class. 62 | * 63 | * @param data The XML data to read. 64 | * @param clazz The class type to deserialize the XML data into. 65 | * @param The generic type of the class. 66 | * @return The deserialized object. 67 | */ 68 | public T read(String data, Class clazz) { 69 | return read(new StringReader(data), clazz); 70 | } 71 | 72 | /** 73 | * Reads XML data from a reader and deserializes it into an object of the specified class. 74 | * 75 | * @param data The reader containing XML data to read. 76 | * @param clazz The class type to deserialize the XML data into. 77 | * @param The generic type of the class. 78 | * @return The deserialized object. 79 | */ 80 | public T read(Reader data, Class clazz) { 81 | PathBasedSaxHandler saxHandler = new PathBasedSaxHandler<>((rootTag) -> pathWriterIndexFactory.createIndexForType(clazz, rootTag), this.configuration); 82 | saxParser.parse(data, saxHandler); 83 | return saxHandler.instance(); 84 | } 85 | 86 | 87 | /** 88 | * Reads XML data and deserializes it into a map with specified key and value types. 89 | * 90 | * @param data The XML data to read. 91 | * @param mapOf The MapOf instance specifying key and value types. 92 | * @param The type of map keys (only supports String). 93 | * @param The type of map values. 94 | * @return The deserialized map. 95 | * @throws XjxDeserializationException If the map key type is not supported (only supports String). 96 | *

97 | * Example usage: 98 | *

{@code
 99 |      * MapOf mapOf = new MapOf() {};
100 |      * Map deserializedMap = new XjxSerdes().read("...", mapOf);
101 |      * }
102 | */ 103 | public Map read(String data, MapOf mapOf) { 104 | return read(new StringReader(data), mapOf); 105 | } 106 | 107 | @SuppressWarnings("unchecked") 108 | public Map read(Reader data, MapOf mapOf) { 109 | Class keyType = mapOf.keyType(); 110 | Class valueType = mapOf.valueType(); 111 | if (keyType == String.class && valueType == Object.class) { 112 | HashMap map = new HashMap<>(); 113 | MapRootSaxHandler mapRootSaxHandler = new MapRootSaxHandler(map, true); 114 | saxParser.parse(data, mapRootSaxHandler); 115 | return (Map) map; 116 | } 117 | throw new XjxDeserializationException("Maps only support String as key"); 118 | } 119 | 120 | /** 121 | * Writes an object to an XML document. 122 | * 123 | * @param data The object to serialize to XML. 124 | * @param The generic type of the object. 125 | * @return The XML representation of the object. 126 | */ 127 | public String write(T data) { 128 | var nodes = xmlNodeStructureFactory.build(data); 129 | return xmlStringBuilder.build(nodes); 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/DeserializationFeature.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize; 2 | 3 | public enum DeserializationFeature { 4 | FAIL_ON_UNMAPPABLE_ENUM_VALUE 5 | 6 | } 7 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/LazySupplier.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize; 2 | 3 | import java.util.function.Supplier; 4 | 5 | public class LazySupplier implements Supplier { 6 | private T instance; 7 | private Supplier initializer; 8 | 9 | public LazySupplier(Supplier initializer) { 10 | this.initializer = initializer; 11 | } 12 | 13 | @Override 14 | public T get() { 15 | if (instance == null) { 16 | instance = initializer.get(); 17 | } 18 | return instance; 19 | } 20 | 21 | public void reset(Supplier supplier) { 22 | this.instance = null; 23 | this.initializer = supplier; 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/MapAsRoot.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize; 2 | 3 | import java.util.Map; 4 | 5 | public record MapAsRoot(Object root, Map map) { 6 | } 7 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/MapOf.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize; 2 | 3 | import java.lang.reflect.ParameterizedType; 4 | import java.lang.reflect.Type; 5 | 6 | public abstract class MapOf { 7 | 8 | protected final Type keyType; 9 | 10 | protected final Type valueType; 11 | 12 | protected MapOf() { 13 | Type superClass = this.getClass().getGenericSuperclass(); 14 | this.keyType = ((ParameterizedType) superClass).getActualTypeArguments()[0]; 15 | this.valueType = ((ParameterizedType) superClass).getActualTypeArguments()[1]; 16 | } 17 | 18 | @SuppressWarnings("unchecked") 19 | public Class keyType() { 20 | return (Class) keyType; 21 | } 22 | 23 | @SuppressWarnings("unchecked") 24 | public Class valueType() { 25 | return (Class) valueType; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/MapRootSaxHandler.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize; 2 | 3 | import java.util.HashMap; 4 | import java.util.LinkedHashMap; 5 | import java.util.LinkedList; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | import io.jonasg.xjx.sax.Attribute; 10 | import io.jonasg.xjx.sax.SaxHandler; 11 | 12 | public class MapRootSaxHandler implements SaxHandler { 13 | 14 | private final LinkedList> mapsStack; 15 | 16 | private final boolean skipRootTag; 17 | 18 | private Map instance; 19 | 20 | private String characterData; 21 | 22 | private String prevStartTag; 23 | 24 | private String rootTag; 25 | 26 | public MapRootSaxHandler(Map instance) { 27 | this.mapsStack = new LinkedList<>(); 28 | this.mapsStack.add(instance); 29 | this.skipRootTag = false; 30 | this.instance = instance; 31 | } 32 | 33 | public MapRootSaxHandler(HashMap instance, boolean skipRootTag) { 34 | this.mapsStack = new LinkedList<>(); 35 | this.mapsStack.add(instance); 36 | this.skipRootTag = skipRootTag; 37 | } 38 | 39 | @Override 40 | public void startDocument() { 41 | } 42 | 43 | @Override 44 | public void startTag(String namespace, String name, List attributes) { 45 | if (this.rootTag != null || !skipRootTag) { 46 | Map activeMap = this.mapsStack.getLast(); 47 | Map newMap = new LinkedHashMap<>(); 48 | activeMap.put(name, newMap); 49 | this.mapsStack.add(newMap); 50 | this.prevStartTag = name; 51 | } 52 | if (this.rootTag == null) { 53 | this.rootTag = name; 54 | } 55 | } 56 | 57 | @Override 58 | public void endTag(String namespace, String name) { 59 | if (name.equals(this.prevStartTag)) { 60 | this.mapsStack.removeLast(); 61 | } 62 | if (characterData != null) { 63 | Map currentMap = this.mapsStack.getLast(); 64 | currentMap.put(name, characterData); 65 | this.characterData = null; 66 | } else { 67 | this.mapsStack.removeLast(); 68 | } 69 | } 70 | 71 | @Override 72 | public void characters(String data) { 73 | this.characterData = data; 74 | } 75 | 76 | public Map instance() { 77 | return instance; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/MapWithTypeInfo.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize; 2 | 3 | import java.util.Map; 4 | 5 | public record MapWithTypeInfo(Map map, Class valueType) { 6 | } 7 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathBasedSaxHandler.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize; 2 | 3 | import java.util.HashMap; 4 | import java.util.LinkedList; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.function.Function; 8 | 9 | import io.jonasg.xjx.sax.Attribute; 10 | import io.jonasg.xjx.sax.SaxHandler; 11 | import io.jonasg.xjx.serdes.Path; 12 | import io.jonasg.xjx.serdes.deserialize.config.XjxConfiguration; 13 | 14 | public class PathBasedSaxHandler implements SaxHandler { 15 | 16 | private final Function indexSupplier; 17 | 18 | private final XjxConfiguration configuration; 19 | 20 | private final LinkedList objectInstances = new LinkedList<>(); 21 | 22 | private String rootTag; 23 | 24 | private Path path; 25 | 26 | private PathWriterIndex pathWriterIndex; 27 | 28 | private String data; 29 | 30 | private SaxHandler mapRootSaxHandlerDelegate; 31 | 32 | private String mapStartTag; 33 | 34 | public PathBasedSaxHandler(Function indexSupplier, XjxConfiguration configuration) { 35 | this.indexSupplier = indexSupplier; 36 | this.configuration = configuration; 37 | } 38 | 39 | public PathBasedSaxHandler(Function indexSupplier, String rootTag, XjxConfiguration configuration) { 40 | this.indexSupplier = indexSupplier; 41 | this.rootTag = rootTag; 42 | this.configuration = configuration; 43 | handleRootTag(rootTag); 44 | } 45 | 46 | @Override 47 | public void startDocument() { 48 | } 49 | 50 | @Override 51 | @SuppressWarnings("unchecked") 52 | public void startTag(String namespace, String name, List attributes) { 53 | if (this.mapRootSaxHandlerDelegate != null) { 54 | this.mapRootSaxHandlerDelegate.startTag(namespace, name, attributes); 55 | } 56 | if (this.rootTag == null) { 57 | handleRootTag(name); 58 | } else { 59 | this.path = path.append(name); 60 | List pathWriters = pathWriterIndex.get(path); 61 | if (pathWriters != null) { 62 | pathWriters.forEach(pathWriter -> { 63 | if (pathWriter.getObjectInitializer() != null) { 64 | Object object = pathWriter.getObjectInitializer().get(); 65 | if (object instanceof Map) { 66 | this.mapRootSaxHandlerDelegate = new MapRootSaxHandler((HashMap) object); 67 | this.mapStartTag = name; 68 | } else if (object instanceof MapWithTypeInfo mapWithTypeInfo) { 69 | this.mapRootSaxHandlerDelegate = new TypedValueMapSaxHandler(mapWithTypeInfo, configuration); 70 | this.mapStartTag = name; 71 | } 72 | this.objectInstances.push(object); 73 | } 74 | }); 75 | } 76 | attributes.forEach(a -> { 77 | List attributeWriters = pathWriterIndex.get(path.appendAttribute(a.name())); 78 | if (attributeWriters != null) { 79 | attributeWriters.stream().forEach(attributeWriter -> attributeWriter.getValueInitializer().accept(a.value())); 80 | } 81 | }); 82 | } 83 | } 84 | 85 | @Override 86 | public void endTag(String namespace, String name) { 87 | if (this.mapRootSaxHandlerDelegate != null) { 88 | if (name.equals(mapStartTag)) { 89 | this.mapRootSaxHandlerDelegate = null; 90 | } else { 91 | this.mapRootSaxHandlerDelegate.endTag(namespace, name); 92 | } 93 | } 94 | List pathWriters = pathWriterIndex.get(path); 95 | if (pathWriters != null) { 96 | pathWriters.forEach(pathWriter -> { 97 | if (data != null) { 98 | pathWriter.getValueInitializer().accept(data); 99 | } 100 | if (pathWriter.getObjectInitializer() != null && !objectInstances.isEmpty() && objectInstances.size() != 1) { 101 | if (pathWriter.getValueInitializer() != null) { 102 | pathWriter.getValueInitializer().accept(objectInstances.peek()); 103 | } 104 | objectInstances.pop(); 105 | } 106 | }); 107 | } 108 | data = null; 109 | path = path.pop(); 110 | } 111 | 112 | @Override 113 | public void characters(String data) { 114 | if (this.mapRootSaxHandlerDelegate != null) { 115 | this.mapRootSaxHandlerDelegate.characters(data); 116 | } 117 | this.data = data; 118 | } 119 | 120 | private void handleRootTag(String name) { 121 | this.pathWriterIndex = indexSupplier.apply(name); 122 | this.rootTag = name; 123 | path = Path.of(name); 124 | List pathWriters = pathWriterIndex.get(path); 125 | if (pathWriters != null) { 126 | pathWriters.forEach(pathWriter -> { 127 | Object parent = pathWriter.getRootInitializer().get(); 128 | if (parent instanceof MapAsRoot mapAsRoot) { 129 | this.mapRootSaxHandlerDelegate = new MapRootSaxHandler(mapAsRoot.map()); 130 | this.objectInstances.push(mapAsRoot.root()); 131 | } else { 132 | this.objectInstances.push(parent); 133 | } 134 | }); 135 | } 136 | } 137 | 138 | @SuppressWarnings("unchecked") 139 | public T instance() { 140 | Object instance = objectInstances.pop(); 141 | if (instance instanceof RecordWrapper recordWrapper) { 142 | return (T) recordWrapper.record(); 143 | } 144 | return (T) instance; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathWriter.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize; 2 | 3 | import java.util.function.Consumer; 4 | import java.util.function.Supplier; 5 | 6 | public class PathWriter { 7 | 8 | private Supplier rootInitializer; 9 | 10 | private Supplier objectInitializer; 11 | 12 | private Consumer valueInitializer; 13 | 14 | public static PathWriter rootInitializer(Supplier rootInitializer) { 15 | PathWriter pathWriter = new PathWriter(); 16 | pathWriter.rootInitializer = rootInitializer; 17 | return pathWriter; 18 | } 19 | 20 | public static PathWriter objectInitializer(Supplier objectInitializer) { 21 | PathWriter pathWriter = new PathWriter(); 22 | pathWriter.objectInitializer = objectInitializer; 23 | return pathWriter; 24 | } 25 | 26 | public void setRootInitializer(Supplier rootInitializer) { 27 | this.rootInitializer = rootInitializer; 28 | } 29 | 30 | public PathWriter setValueInitializer(Consumer valueInitializer) { 31 | this.valueInitializer = valueInitializer; 32 | return this; 33 | } 34 | 35 | public static PathWriter valueInitializer(Consumer o) { 36 | PathWriter pathWriter = new PathWriter(); 37 | pathWriter.valueInitializer = o; 38 | return pathWriter; 39 | } 40 | 41 | public Supplier getRootInitializer() { 42 | return rootInitializer; 43 | } 44 | 45 | public Supplier getObjectInitializer() { 46 | return objectInitializer; 47 | } 48 | 49 | public Consumer getValueInitializer() { 50 | return valueInitializer; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/PathWriterIndex.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize; 2 | 3 | import io.jonasg.xjx.serdes.Path; 4 | 5 | import java.util.ArrayList; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | public class PathWriterIndex { 11 | 12 | private final Map> index = new HashMap<>(); 13 | 14 | public void put(Path path, PathWriter pathWriter) { 15 | index.compute(path, (p, w) -> { 16 | if (w == null) { 17 | List pathWriters = new ArrayList<>(); 18 | pathWriters.add(pathWriter); 19 | return pathWriters; 20 | } 21 | w.add(pathWriter); 22 | return w; 23 | }); 24 | } 25 | 26 | public void putAll(PathWriterIndex pathWriterIndex) { 27 | index.putAll(pathWriterIndex.index); 28 | } 29 | 30 | public List get(Path path) { 31 | return index.get(path); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/RecordWrapper.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize; 2 | 3 | import java.lang.reflect.Constructor; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | public class RecordWrapper { 8 | private final Map fieldMapping = new HashMap<>(); 9 | 10 | private final Class type; 11 | 12 | public RecordWrapper(Class type) { 13 | this.type = type; 14 | } 15 | 16 | public void set(String name, Object value) { 17 | this.fieldMapping.put(name, value); 18 | } 19 | 20 | @SuppressWarnings("unchecked") 21 | public T record() { 22 | try { 23 | Constructor[] constructors = type.getDeclaredConstructors(); 24 | 25 | Constructor constructor = constructors[0]; 26 | constructor.setAccessible(true); 27 | 28 | Object[] args = new Object[constructor.getParameterCount()]; 29 | var parameters = constructor.getParameters(); 30 | for (int i = 0; i < parameters.length; i++) { 31 | String paramName = parameters[i].getName(); 32 | Class paramType = parameters[i].getType(); 33 | Object paramValue = fieldMapping.get(paramName); 34 | if (paramValue != null) { 35 | args[i] = paramValue; 36 | } 37 | else { 38 | if (paramType == boolean.class) { 39 | args[i] = false; 40 | } 41 | else if (paramType == long.class) { 42 | args[i] = 0; 43 | } 44 | else if (paramType == double.class) { 45 | args[i] = 0; 46 | } 47 | else if (paramType == char.class) { 48 | args[i] = '\000'; 49 | } 50 | else { 51 | args[i] = null; 52 | } 53 | } 54 | } 55 | 56 | return (T) constructor.newInstance(args); 57 | 58 | } 59 | catch (Exception e) { 60 | throw new RuntimeException("Error creating record", e); 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/TagPath.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize; 2 | 3 | import io.jonasg.xjx.serdes.Tag; 4 | import io.jonasg.xjx.serdes.reflector.FieldReflector; 5 | 6 | public class TagPath { 7 | 8 | private final Tag tag; 9 | private final FieldReflector field; 10 | 11 | public TagPath(Tag tag, FieldReflector field) { 12 | this.tag = tag; 13 | this.field = field; 14 | } 15 | 16 | public boolean isAbsolute() { 17 | return tag.path().trim().startsWith("/"); 18 | } 19 | 20 | public FieldReflector field() { 21 | return field; 22 | } 23 | 24 | public String path() { 25 | return tag.path().trim(); 26 | } 27 | 28 | public String attribute() { 29 | return tag.attribute(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/TypedValueMapSaxHandler.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | import java.util.Objects; 6 | 7 | import io.jonasg.xjx.sax.Attribute; 8 | import io.jonasg.xjx.sax.SaxHandler; 9 | import io.jonasg.xjx.serdes.deserialize.config.XjxConfiguration; 10 | 11 | public class TypedValueMapSaxHandler implements SaxHandler { 12 | 13 | private final Class valueType; 14 | 15 | private final XjxConfiguration configuration; 16 | 17 | private final Map instance; 18 | 19 | private PathBasedSaxHandler objectPathBasedSaxHandler; 20 | 21 | private String activeKey; 22 | 23 | public TypedValueMapSaxHandler(MapWithTypeInfo instance, XjxConfiguration configuration) { 24 | this.instance = instance.map(); 25 | this.valueType = instance.valueType(); 26 | this.configuration = configuration; 27 | } 28 | 29 | @Override 30 | public void startDocument() { 31 | } 32 | 33 | @Override 34 | public void startTag(String namespace, String name, List attributes) { 35 | if (this.activeKey == null) { 36 | this.activeKey = name; 37 | objectPathBasedSaxHandler = new PathBasedSaxHandler<>(rootTag -> 38 | new PathWriterIndexFactory(configuration).createIndexForType(valueType, this.activeKey), this.activeKey, configuration); 39 | } else { 40 | objectPathBasedSaxHandler.startTag(namespace, name, attributes); 41 | } 42 | } 43 | 44 | @Override 45 | public void endTag(String namespace, String name) { 46 | if (Objects.equals(this.activeKey, name)) { 47 | this.instance.put(this.activeKey, objectPathBasedSaxHandler.instance()); 48 | this.activeKey = null; 49 | } else { 50 | objectPathBasedSaxHandler.endTag(namespace, name); 51 | } 52 | } 53 | 54 | @Override 55 | public void characters(String data) { 56 | objectPathBasedSaxHandler.characters(data); 57 | } 58 | 59 | public Map instance() { 60 | return instance; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/ValueDeserialization.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.FIELD) 10 | public @interface ValueDeserialization { 11 | 12 | Class> value(); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/ValueDeserializationHandler.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize; 2 | 3 | import java.lang.reflect.Field; 4 | import java.lang.reflect.InvocationTargetException; 5 | import java.util.Optional; 6 | 7 | public class ValueDeserializationHandler { 8 | 9 | private static ValueDeserializationHandler instance; 10 | 11 | public static ValueDeserializationHandler getInstance() { 12 | if (instance == null) { 13 | instance = new ValueDeserializationHandler(); 14 | } 15 | return instance; 16 | } 17 | 18 | public Optional handle(Field field, String value) { 19 | ValueDeserialization valueDeserialization = field.getAnnotation(ValueDeserialization.class); 20 | if (valueDeserialization != null) { 21 | try { 22 | var valueDeserializer = valueDeserialization.value().getConstructor().newInstance(); 23 | return Optional.of(valueDeserializer.deserializer(value)); 24 | } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | 25 | IllegalAccessException e) { 26 | throw new RuntimeException(e); 27 | } 28 | 29 | } 30 | return Optional.empty(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/ValueDeserializer.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize; 2 | 3 | public interface ValueDeserializer { 4 | T deserializer(String value); 5 | } 6 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/XjxDeserializationException.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize; 2 | 3 | public class XjxDeserializationException extends RuntimeException { 4 | public XjxDeserializationException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/accessor/FieldAccessor.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize.accessor; 2 | 3 | import io.jonasg.xjx.serdes.TypeMappers; 4 | import io.jonasg.xjx.serdes.deserialize.RecordWrapper; 5 | import io.jonasg.xjx.serdes.deserialize.config.XjxConfiguration; 6 | import io.jonasg.xjx.serdes.reflector.FieldReflector; 7 | 8 | public interface FieldAccessor { 9 | 10 | static FieldAccessor of(FieldReflector field, Object instance, XjxConfiguration configuration) { 11 | if (instance instanceof RecordWrapper recordWrapper) { 12 | return new RecordFieldAccessor(field, recordWrapper, configuration); 13 | } else { 14 | var setterFieldAccessor = new SetterFieldAccessor(field, instance); 15 | if (setterFieldAccessor.hasSetterForField()) { 16 | return new SetterFieldAccessor(field, instance); 17 | } 18 | var mapper = TypeMappers.forType(field.type(), configuration); 19 | return new ReflectiveFieldAccessor(field, instance, mapper); 20 | } 21 | } 22 | 23 | void set(Object value); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/accessor/RecordFieldAccessor.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize.accessor; 2 | 3 | import io.jonasg.xjx.serdes.TypeMappers; 4 | import io.jonasg.xjx.serdes.deserialize.RecordWrapper; 5 | import io.jonasg.xjx.serdes.deserialize.config.XjxConfiguration; 6 | import io.jonasg.xjx.serdes.reflector.FieldReflector; 7 | 8 | public class RecordFieldAccessor implements FieldAccessor { 9 | 10 | private final FieldReflector field; 11 | 12 | private final RecordWrapper recordWrapper; 13 | 14 | private final XjxConfiguration configuration; 15 | 16 | public RecordFieldAccessor(FieldReflector field, RecordWrapper recordWrapper, XjxConfiguration configuration) { 17 | this.field = field; 18 | this.recordWrapper = recordWrapper; 19 | this.configuration = configuration; 20 | } 21 | 22 | @Override 23 | public void set(Object value) { 24 | Object mappedValue = TypeMappers.forType(field.type(), configuration).apply(value); 25 | recordWrapper.set(field.rawField().getName(), mappedValue); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/accessor/ReflectiveFieldAccessor.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize.accessor; 2 | 3 | import java.util.function.Function; 4 | 5 | import io.jonasg.xjx.serdes.reflector.FieldReflector; 6 | 7 | public class ReflectiveFieldAccessor implements FieldAccessor { 8 | 9 | private final FieldReflector field; 10 | private final Object instance; 11 | private final Function mapper; 12 | 13 | public ReflectiveFieldAccessor(FieldReflector field, Object instance, Function mapper) { 14 | this.field = field; 15 | this.instance = instance; 16 | this.mapper = mapper; 17 | } 18 | 19 | @Override 20 | public void set(Object value) { 21 | field.set(instance, mapper.apply(value)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/accessor/SetterFieldAccessor.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize.accessor; 2 | 3 | import java.lang.reflect.InvocationTargetException; 4 | import java.lang.reflect.Method; 5 | import java.util.Arrays; 6 | 7 | import io.jonasg.xjx.serdes.reflector.FieldReflector; 8 | 9 | public class SetterFieldAccessor implements FieldAccessor { 10 | 11 | private final Object instance; 12 | 13 | private final Method method; 14 | 15 | public SetterFieldAccessor(FieldReflector field, Object instance) { 16 | String name = field.name(); 17 | this.instance = instance; 18 | this.method = Arrays.stream(instance.getClass().getMethods()) 19 | .filter(method -> method.getName().equals("set" + name.substring(0, 1).toUpperCase() + name.substring(1))) 20 | .findFirst() 21 | .orElse(null); 22 | } 23 | 24 | @Override 25 | public void set(Object value) { 26 | try { 27 | method.invoke(instance, value); 28 | } catch (IllegalAccessException | InvocationTargetException e) { 29 | throw new RuntimeException(e); 30 | } 31 | } 32 | 33 | public boolean hasSetterForField() { 34 | return method != null; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/config/ConfigurationBuilder.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize.config; 2 | 3 | import io.jonasg.xjx.serdes.XjxSerdes; 4 | 5 | @SuppressWarnings("UnusedReturnValue") 6 | public class ConfigurationBuilder { 7 | 8 | private final XjxConfiguration xjxConfiguration; 9 | 10 | public ConfigurationBuilder(XjxConfiguration xjxConfiguration) { 11 | this.xjxConfiguration = xjxConfiguration; 12 | } 13 | 14 | /** 15 | * Configures the {@link XjxSerdes} to fail when an enum value cannot be mapped to an enum constant. 16 | * When not set, defaults to false and will default to null when a value cannot be mapped to a name. 17 | * @param failOnUnmappableEnumValue Whether to fail when an enum value cannot be mapped to an enum constant 18 | * @return The ConfigurationBuilder 19 | */ 20 | public ConfigurationBuilder failOnUnknownEnumValue(boolean failOnUnmappableEnumValue) { 21 | this.xjxConfiguration.failOnUnknownEnumValue = failOnUnmappableEnumValue; 22 | return this; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/deserialize/config/XjxConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize.config; 2 | 3 | public class XjxConfiguration { 4 | 5 | /** 6 | * Whether to fail when an enum value cannot be mapped to an enum constant 7 | * Defaults to false and will default to null when a value cannot be mapped to a name. 8 | */ 9 | boolean failOnUnknownEnumValue = false; 10 | 11 | public boolean failOnUnknownEnumValue() { 12 | return this.failOnUnknownEnumValue; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/FieldReflector.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.reflector; 2 | 3 | import java.lang.annotation.Annotation; 4 | import java.lang.reflect.Field; 5 | import java.lang.reflect.Type; 6 | import java.util.StringJoiner; 7 | 8 | public class FieldReflector { 9 | 10 | private final Field field; 11 | 12 | public FieldReflector(Field field) { 13 | this.field = field; 14 | } 15 | 16 | public String name() { 17 | return field.getName(); 18 | } 19 | 20 | public void set(T instance, Object value) { 21 | try { 22 | field.setAccessible(true); 23 | field.set(instance, value); 24 | } catch (IllegalAccessException e) { 25 | throw new RuntimeException(e); 26 | } 27 | } 28 | 29 | @SuppressWarnings("unchecked") 30 | public Class type() { 31 | return (Class) field.getType(); 32 | } 33 | 34 | public boolean isOfType(Class type) { 35 | return type() == type; 36 | } 37 | 38 | public Field rawField() { 39 | return field; 40 | } 41 | 42 | public Type genericType() { 43 | return field.getGenericType(); 44 | } 45 | 46 | public T getAnnotation(Class clazz) { 47 | return field.getAnnotation(clazz); 48 | } 49 | 50 | public boolean hasAnnotation(Class annotation) { 51 | return field.getAnnotation(annotation) != null; 52 | } 53 | 54 | public boolean isRecord() { 55 | return field.getType().isRecord(); 56 | } 57 | 58 | @Override 59 | public String toString() { 60 | return new StringJoiner(", ", FieldReflector.class.getSimpleName() + "[", "]") 61 | .add("field=" + field) 62 | .toString(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/InstanceField.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.reflector; 2 | 3 | import java.lang.annotation.Annotation; 4 | import java.util.StringJoiner; 5 | 6 | public class InstanceField { 7 | private final FieldReflector fieldReflector; 8 | private final Object instance; 9 | 10 | public InstanceField(FieldReflector fieldReflector, T instance) { 11 | this.fieldReflector = fieldReflector; 12 | this.instance = instance; 13 | } 14 | 15 | public boolean hasAnnotation(Class annotation) { 16 | return fieldReflector.hasAnnotation(annotation); 17 | } 18 | 19 | public T getAnnotation(Class type) { 20 | return fieldReflector.getAnnotation(type); 21 | } 22 | 23 | public Object getValue() { 24 | try { 25 | fieldReflector.rawField().setAccessible(true); 26 | Object value = fieldReflector.rawField().get(instance); 27 | fieldReflector.rawField().setAccessible(false); 28 | return value; 29 | } catch (IllegalAccessException e) { 30 | throw new RuntimeException(e); 31 | } 32 | } 33 | 34 | public Class type() { 35 | return fieldReflector.type(); 36 | } 37 | 38 | public InstanceReflector reflect() { 39 | return new InstanceReflector<>(getValue()); 40 | } 41 | 42 | @Override 43 | public String toString() { 44 | return new StringJoiner(", ", InstanceField.class.getSimpleName() + "[", "]") 45 | .add("fieldReflector=" + fieldReflector) 46 | .add("instance=" + instance) 47 | .toString(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/InstanceReflector.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.reflector; 2 | 3 | import java.lang.reflect.Constructor; 4 | import java.lang.reflect.InvocationTargetException; 5 | import java.util.Arrays; 6 | import java.util.List; 7 | import java.util.function.Predicate; 8 | 9 | public class InstanceReflector { 10 | 11 | private final Class type; 12 | private final TypeReflector typeReflector; 13 | 14 | private final T instance; 15 | 16 | public InstanceReflector(Class type, TypeReflector typeReflector) { 17 | this.type = type; 18 | this.typeReflector = typeReflector; 19 | this.instance = newInstance(); 20 | } 21 | 22 | @SuppressWarnings("unchecked") 23 | public InstanceReflector(T instance) { 24 | this.type = (Class) instance.getClass(); 25 | this.typeReflector = TypeReflector.reflect((Class)instance.getClass()); 26 | this.instance = instance; 27 | } 28 | 29 | @SuppressWarnings("unchecked") 30 | private T newInstance() { 31 | Constructor constructor = Arrays.stream(type.getDeclaredConstructors()) 32 | .filter(c -> c.getParameters().length == 0) 33 | .findFirst() 34 | .orElseThrow(); 35 | try { 36 | return (T) constructor.newInstance(); 37 | } catch (InvocationTargetException | InstantiationException e) { 38 | throw new RuntimeException(e); 39 | } catch (IllegalAccessException e) { 40 | constructor.setAccessible(true); 41 | T instance; 42 | try { 43 | instance = (T) constructor.newInstance(); 44 | } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) { 45 | throw new RuntimeException(ex); 46 | } 47 | constructor.setAccessible(false); 48 | return instance; 49 | } 50 | } 51 | 52 | public void setField(String fieldName, Object value) { 53 | typeReflector.field(fieldName) 54 | .ifPresent(f -> f.set(instance, value)); 55 | } 56 | 57 | public List fields(Predicate predicate) { 58 | return typeReflector.fields() 59 | .stream() 60 | .map(f -> new InstanceField(f, instance)) 61 | .filter(predicate) 62 | .toList(); 63 | } 64 | 65 | public List fields() { 66 | return fields(f -> true); 67 | } 68 | 69 | public T instance() { 70 | return instance; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/Reflector.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.reflector; 2 | 3 | public class Reflector { 4 | 5 | public static InstanceReflector reflect(T instance) { 6 | return new InstanceReflector<>(instance); 7 | } 8 | 9 | public static TypeReflector reflect(Class type) { 10 | return new TypeReflector<>(type); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/reflector/TypeReflector.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.reflector; 2 | 3 | import java.lang.annotation.Annotation; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | import java.util.Optional; 7 | 8 | public class TypeReflector { 9 | 10 | private final Class clazz; 11 | 12 | private final List fields; 13 | 14 | public TypeReflector(Class clazz) { 15 | this.clazz = clazz; 16 | this.fields = Arrays.stream(clazz.getDeclaredFields()).map(FieldReflector::new).toList(); 17 | } 18 | 19 | public static TypeReflector reflect(Class clazz) { 20 | return new TypeReflector<>(clazz); 21 | } 22 | 23 | public InstanceReflector instanceReflector() { 24 | return new InstanceReflector<>(clazz, this); 25 | } 26 | 27 | public Optional field(String fieldName) { 28 | return this.fields.stream() 29 | .filter(f -> f.name().equals(fieldName)) 30 | .findFirst(); 31 | } 32 | 33 | public List fields() { 34 | return this.fields; 35 | } 36 | 37 | public E annotation(Class annotation) { 38 | return clazz.getAnnotation(annotation); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/serialize/XmlNode.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.serialize; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Objects; 6 | 7 | import io.jonasg.xjx.Attributes; 8 | 9 | public final class XmlNode { 10 | private final String name; 11 | private Object value; 12 | private final List children; 13 | private final Attributes attributes; 14 | 15 | public XmlNode(String name, Object value, List children, Attributes attributes) { 16 | this.name = name; 17 | this.value = value; 18 | this.children = children; 19 | this.attributes = attributes; 20 | } 21 | 22 | public XmlNode(String name) { 23 | this(name, null, new ArrayList<>(), new Attributes()); 24 | } 25 | 26 | public XmlNode(String name, Object value) { 27 | this(name, value, null, new Attributes()); 28 | } 29 | 30 | public void addValueNode(String name, Object value) { 31 | this.children.stream() 32 | .filter(n -> Objects.equals(n.name, name)) 33 | .findFirst() 34 | .map(n -> n.value = value) 35 | .orElseGet(() -> { 36 | var node = new XmlNode(name, value); 37 | this.children.add(node); 38 | return node; 39 | }); 40 | } 41 | 42 | public XmlNode addNode(String name) { 43 | return this.children.stream() 44 | .filter(n -> Objects.equals(n.name, name)) 45 | .findFirst() 46 | .orElseGet(() -> { 47 | var node = new XmlNode(name); 48 | this.children.add(node); 49 | return node; 50 | }); 51 | } 52 | 53 | public void addAttribute(String attribute, Object value) { 54 | this.attributes.add(attribute, String.valueOf(value)); 55 | } 56 | 57 | public boolean hasChildren() { 58 | return this.children != null && !this.children.isEmpty(); 59 | } 60 | 61 | public boolean containsAValue() { 62 | return value != null; 63 | } 64 | 65 | public boolean hasAttributes() { 66 | return !this.attributes.isEmpty(); 67 | } 68 | 69 | public String name() { 70 | return name; 71 | } 72 | 73 | public Object value() { 74 | return value; 75 | } 76 | 77 | public List children() { 78 | return children; 79 | } 80 | 81 | public Attributes attributes() { 82 | return attributes; 83 | } 84 | 85 | @Override 86 | public boolean equals(Object obj) { 87 | if (obj == this) return true; 88 | if (obj == null || obj.getClass() != this.getClass()) return false; 89 | var that = (XmlNode) obj; 90 | return Objects.equals(this.name, that.name) && 91 | Objects.equals(this.value, that.value) && 92 | Objects.equals(this.children, that.children) && 93 | Objects.equals(this.attributes, that.attributes); 94 | } 95 | 96 | @Override 97 | public int hashCode() { 98 | return Objects.hash(name, value, children, attributes); 99 | } 100 | 101 | @Override 102 | public String toString() { 103 | return "XmlNode[" + 104 | "name=" + name + ", " + 105 | "value=" + value + ", " + 106 | "children=" + children + ", " + 107 | "attributes=" + attributes + ']'; 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/serialize/XmlNodeStructureFactory.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.serialize; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.LocalDate; 5 | import java.time.LocalDateTime; 6 | import java.time.ZonedDateTime; 7 | import java.util.List; 8 | 9 | import io.jonasg.xjx.serdes.Path; 10 | import io.jonasg.xjx.serdes.Section; 11 | import io.jonasg.xjx.serdes.Tag; 12 | import io.jonasg.xjx.serdes.reflector.InstanceField; 13 | import io.jonasg.xjx.serdes.reflector.Reflector; 14 | 15 | public class XmlNodeStructureFactory { 16 | 17 | public static final List> BASIC_TYPES = List.of( 18 | String.class, Integer.class, Boolean.class, boolean.class, Long.class, long.class, BigDecimal.class, Double.class, 19 | double.class, char.class, Character.class, LocalDate.class, LocalDateTime.class, ZonedDateTime.class, byte[].class); 20 | 21 | public XmlNode build(T data) { 22 | return getXmlNode(Path.parse("/"), data, null); 23 | } 24 | 25 | private XmlNode getXmlNode(Path parentPath, T data, XmlNode node) { 26 | if (data != null && !BASIC_TYPES.contains(data.getClass()) && !data.getClass().isEnum()) { 27 | List fields = Reflector.reflect(data).fields(); 28 | node = buildNodeForFields(parentPath, node, fields); 29 | } 30 | return node; 31 | } 32 | 33 | private XmlNode buildNodeForFields(Path parentPath, XmlNode node, List fields) { 34 | for (InstanceField field : fields) { 35 | if (field.hasAnnotation(Tag.class)) { 36 | node = buildNodeForField(field, parentPath, node); 37 | } 38 | else if (!BASIC_TYPES.contains(field.type())) { 39 | return buildNodeForFields(parentPath, node, field.reflect().fields()); 40 | } 41 | } 42 | return node; 43 | } 44 | 45 | private XmlNode buildNodeForField(InstanceField field, Path parentPath, XmlNode rootNode) { 46 | var tag = field.getAnnotation(Tag.class); 47 | var path = parentPath.append(Path.parse(tag.path())); 48 | if (rootNode == null) { 49 | rootNode = new XmlNode(path.getRoot()); 50 | } 51 | var node = rootNode; 52 | for (int i = 1; i < path.size(); i++) { 53 | Section section = path.getSection(i); 54 | if (section.isLeaf()) { 55 | handleLeafNode(field, section, tag, node); 56 | } 57 | else { 58 | node = node.addNode(section.name()); 59 | } 60 | } 61 | return getXmlNode(path, field.getValue(), rootNode); 62 | } 63 | 64 | private void handleLeafNode(InstanceField field, Section section, Tag tag, XmlNode node) { 65 | if (!tag.attribute().isEmpty()) { 66 | if (field.getValue() != null) { 67 | node.addNode(section.name()) 68 | .addAttribute(tag.attribute(), field.getValue()); 69 | } 70 | } 71 | else { 72 | node.addValueNode(section.name(), field.getValue()); 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /xjx-serdes/src/main/java/io/jonasg/xjx/serdes/serialize/XmlStringBuilder.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.serialize; 2 | 3 | import java.util.List; 4 | 5 | public class XmlStringBuilder { 6 | 7 | public String build(XmlNode nodes) { 8 | var sb = new StringBuilder(); 9 | sb.append("<").append(nodes.name()).append(">\n"); 10 | buildNodes(nodes.children(), sb); 11 | sb.append("\n"); 12 | return sb.toString(); 13 | } 14 | 15 | private void buildNodes(List nodes, StringBuilder sb) { 16 | int indentationLevel = 1; 17 | buildNodes(nodes, sb, indentationLevel); 18 | } 19 | 20 | private void buildNodes(List nodes, StringBuilder sb, int indentationLevel) { 21 | String indentation = " ".repeat(indentationLevel); 22 | 23 | nodes.forEach(node -> { 24 | sb.append(indentation) 25 | .append("<").append(node.name()); 26 | 27 | if (node.hasAttributes()) { 28 | node.attributes().stream().forEach(attribute -> 29 | sb.append(" ") 30 | .append(attribute.name()) 31 | .append("=\"") 32 | .append(attribute.value()) 33 | .append("\"")); 34 | } 35 | 36 | if (node.hasChildren() || node.containsAValue()) { 37 | sb.append(">"); 38 | if (node.containsAValue()) { 39 | sb.append(node.value()); 40 | } 41 | if (node.hasChildren()) { 42 | sb.append("\n"); 43 | buildNodes(node.children(), sb, indentationLevel + 1); 44 | sb.append(indentation); 45 | } 46 | sb.append("\n"); 47 | } else { 48 | sb.append("/>\n"); 49 | } 50 | }); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/ComplexTypeDeserializationTest.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize; 2 | 3 | import org.assertj.core.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import io.jonasg.xjx.serdes.Tag; 7 | import io.jonasg.xjx.serdes.XjxSerdes; 8 | 9 | public class ComplexTypeDeserializationTest { 10 | @Test 11 | void mapFieldsOnComplexType() { 12 | // given 13 | String data = """ 14 | 15 | 16 | 17 | New York 18 | 19 | USA 20 | 21 | 22 | 23 | 24 | 75 25 | °F 26 | 27 | 28 | 60 29 | % 30 | 31 | Sunny 32 | 33 | 34 | 35 | 36 | 78 37 | °F 38 | 39 | 40 | 62 41 | °F 42 | 43 | 44 | 10 45 | % 46 | 47 | Partly Cloudy 48 | 49 | 50 | 51 | """; 52 | 53 | // when 54 | WeatherData weatherData = new XjxSerdes().read(data, WeatherData.class); 55 | 56 | // then 57 | Assertions.assertThat(weatherData.location.city).isEqualTo("New York"); 58 | Assertions.assertThat(weatherData.temperature).isEqualTo(75); 59 | } 60 | 61 | public static class WeatherData { 62 | public WeatherData() { 63 | } 64 | 65 | Location location; 66 | 67 | @Tag(path = "/WeatherData/CurrentConditions/Temperature/Value") 68 | Integer temperature; 69 | } 70 | 71 | public static class Location { 72 | public Location() { 73 | } 74 | 75 | @Tag(path = "/WeatherData/Location/City") 76 | String city; 77 | } 78 | 79 | @Test 80 | void mapFieldsIn_Nested_ComplexTypes() { 81 | // given 82 | String data = """ 83 | 84 | 85 | 86 | New York 87 | 88 | USA 89 | 90 | 91 | 92 | 93 | 75 94 | °F 95 | 96 | 97 | 60 98 | % 99 | 100 | Sunny 101 | 102 | 103 | 104 | 105 | 78 106 | °F 107 | 108 | 109 | 62 110 | °F 111 | 112 | 113 | 10 114 | % 115 | 116 | Partly Cloudy 117 | 118 | 119 | 120 | """; 121 | 122 | // when 123 | WeatherDataMultiLevel weatherData = new XjxSerdes().read(data, WeatherDataMultiLevel.class); 124 | 125 | // then 126 | Assertions.assertThat(weatherData.yetAnotherLevel.location.city).isEqualTo("New York"); 127 | Assertions.assertThat(weatherData.yetAnotherLevel.high).isEqualTo("78"); 128 | Assertions.assertThat(weatherData.temperature).isEqualTo(75); 129 | } 130 | 131 | public static class WeatherDataMultiLevel { 132 | public WeatherDataMultiLevel() { 133 | } 134 | 135 | NestedComplexType yetAnotherLevel; 136 | 137 | @Tag(path = "/WeatherData/CurrentConditions/Temperature/Value") 138 | Integer temperature; 139 | } 140 | 141 | public static class NestedComplexType { 142 | LocationMultiLevel location; 143 | 144 | @Tag(path = "/WeatherData/Forecast/Day/High/Value") 145 | String high; 146 | } 147 | 148 | public static class LocationMultiLevel { 149 | public LocationMultiLevel() { 150 | } 151 | 152 | @Tag(path = "/WeatherData/Location/City") 153 | String city; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/CustomValueDeserializationTest.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize; 2 | 3 | import org.assertj.core.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import io.jonasg.xjx.serdes.Tag; 7 | import io.jonasg.xjx.serdes.XjxSerdes; 8 | 9 | public class CustomValueDeserializationTest { 10 | 11 | @Test 12 | void useCustomValueDeserializerOnEnumTypes() { 13 | // given 14 | String data = """ 15 | 16 | 17 | 18 | 19 | 78 20 | fahrenheit 21 | 22 | 23 | 62 24 | fahrenheit 25 | 26 | 27 | 10 28 | percentage 29 | 30 | Partly Cloudy 31 | 32 | 33 | """; 34 | 35 | // when 36 | EnumHolder enumHolder = new XjxSerdes().read(data, EnumHolder.class); 37 | 38 | // then 39 | Assertions.assertThat(enumHolder.dayHighUnit).isEqualTo(Unit.FAHRENHEIT); 40 | Assertions.assertThat(enumHolder.precipitationUnit).isEqualTo(Unit.PERCENTAGE); 41 | } 42 | 43 | static class EnumHolder { 44 | @Tag(path = "/WeatherData/Day/High/Unit") 45 | @ValueDeserialization(UnitDeserialize.class) 46 | Unit dayHighUnit; 47 | 48 | @Tag(path = "/WeatherData/Day/Precipitation/Unit") 49 | @ValueDeserialization(UnitDeserialize.class) 50 | Unit precipitationUnit; 51 | } 52 | 53 | enum Unit { 54 | FAHRENHEIT, PERCENTAGE 55 | } 56 | 57 | static class UnitDeserialize implements ValueDeserializer { 58 | 59 | public UnitDeserialize() { 60 | } 61 | 62 | @Override 63 | public Unit deserializer(String value) { 64 | return Unit.valueOf(value.toUpperCase()); 65 | } 66 | } 67 | 68 | @Test 69 | void useCustomValueDeserializerOnSimpleTypes() { 70 | // given 71 | String data = """ 72 | 73 | 74 | 75 | 76 | 78 77 | celcius 78 | 79 | 80 | 62 81 | fahrenheit 82 | 83 | 84 | 10 85 | percentage 86 | 87 | Partly Cloudy 88 | 89 | 90 | """; 91 | 92 | // when 93 | SimpleTypeHolder simpleTypeHolder = new XjxSerdes().read(data, SimpleTypeHolder.class); 94 | 95 | // then 96 | Assertions.assertThat(simpleTypeHolder.maxTemperature).isEqualTo("78 °C"); 97 | } 98 | 99 | @Test 100 | void useCustomValueDeserializerOnComplexType() { 101 | // given 102 | String data = """ 103 | 104 | 105 | 106 | 107 | 78 108 | celcius 109 | 110 | 111 | 62 112 | fahrenheit 113 | 114 | 115 | 10 116 | percentage 117 | 118 | Partly Cloudy 119 | 120 | 121 | """; 122 | 123 | // when 124 | CustomTypeHolder customTypeHolder = new XjxSerdes().read(data, CustomTypeHolder.class); 125 | 126 | // then 127 | Assertions.assertThat(customTypeHolder.maxTemperatureAsLong).isEqualTo(new Temperature(78)); 128 | } 129 | 130 | static class SimpleTypeHolder { 131 | @Tag(path = "/WeatherData/Day/High/Value") 132 | @ValueDeserialization(SimpleTemperatureDeserializer.class) 133 | String maxTemperature; 134 | } 135 | 136 | static class CustomTypeHolder { 137 | @Tag(path = "/WeatherData/Day/High/Value") 138 | @ValueDeserialization(CustomTemperatureDeserializer.class) 139 | Temperature maxTemperatureAsLong; 140 | } 141 | 142 | public static class SimpleTemperatureDeserializer implements ValueDeserializer { 143 | @Override 144 | public String deserializer(String value) { 145 | return String.format("%s °C", value); 146 | } 147 | } 148 | 149 | public static class CustomTemperatureDeserializer implements ValueDeserializer { 150 | @Override 151 | public Temperature deserializer(String value) { 152 | return Temperature.fahrenheit(value); 153 | } 154 | } 155 | 156 | public static class Temperature { 157 | 158 | private final long value; 159 | 160 | private Temperature(long value) { 161 | this.value = value; 162 | } 163 | 164 | public static Temperature fahrenheit(String value) { 165 | return new Temperature(Long.parseLong(value)); 166 | } 167 | 168 | @Override 169 | public boolean equals(Object o) { 170 | if (this == o) return true; 171 | if (o == null || getClass() != o.getClass()) return false; 172 | 173 | Temperature that = (Temperature) o; 174 | 175 | return value == that.value; 176 | } 177 | 178 | @Override 179 | public int hashCode() { 180 | return (int) (value ^ (value >>> 32)); 181 | } 182 | } 183 | 184 | } 185 | -------------------------------------------------------------------------------- /xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/DeserializationFieldAccessTest.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize; 2 | 3 | import org.assertj.core.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import io.jonasg.xjx.serdes.Tag; 7 | import io.jonasg.xjx.serdes.XjxSerdes; 8 | 9 | public class DeserializationFieldAccessTest { 10 | 11 | @Test 12 | void accessThroughPublicField() { 13 | // given 14 | String data = """ 15 | 16 | 17 | dog 18 | John 19 | 20 | """; 21 | 22 | // when 23 | AnimalWithPublicFields animal = new XjxSerdes().read(data, AnimalWithPublicFields.class); 24 | 25 | // then 26 | Assertions.assertThat(animal.type).isEqualTo("dog"); 27 | Assertions.assertThat(animal.name).isEqualTo("John"); 28 | } 29 | 30 | @Test 31 | void accessThroughPrivateField() { 32 | // given 33 | String data = """ 34 | 35 | 36 | blabal 37 | dog 38 | John 39 | 40 | Canidae 41 | 42 | Canis 43 | 44 | 45 | 46 | """; 47 | 48 | // when 49 | AnimalWithPrivateFields animal = new XjxSerdes().read(data, AnimalWithPrivateFields.class); 50 | 51 | // then 52 | Assertions.assertThat(animal.type).isEqualTo("dog"); 53 | Assertions.assertThat(animal.name).isEqualTo("John"); 54 | Assertions.assertThat(animal.genus.name.value).isEqualTo("Canis"); 55 | Assertions.assertThat(animal.genus.family).isEqualTo("Canidae"); 56 | } 57 | 58 | @Test 59 | void accessThroughSetter() { 60 | // given 61 | String data = """ 62 | 63 | 64 | dog 65 | John 66 | 67 | """; 68 | 69 | // when 70 | AnimalWithPublicSetterFields animal = new XjxSerdes().read(data, AnimalWithPublicSetterFields.class); 71 | 72 | // then 73 | Assertions.assertThat(animal.type).isEqualTo("dog"); 74 | Assertions.assertThat(animal.typeSetThroughSetter).isTrue(); 75 | Assertions.assertThat(animal.name).isEqualTo("John"); 76 | Assertions.assertThat(animal.nameSetThroughSetter).isTrue(); 77 | } 78 | 79 | public static class AnimalWithPublicFields { 80 | public AnimalWithPublicFields() { 81 | } 82 | 83 | @Tag(path = "/Animal/name") 84 | String name; 85 | 86 | @Tag(path = "/Animal/type") 87 | String type; 88 | } 89 | 90 | public static class AnimalWithPrivateFields { 91 | public AnimalWithPrivateFields() { 92 | } 93 | 94 | @Tag(path = "/Animal/type") 95 | private String type; 96 | 97 | @Tag(path = "/Animal/name") 98 | private String name; 99 | 100 | private Genus genus; 101 | } 102 | 103 | public static class Genus { 104 | @Tag(path = "/Animal/genus/family") 105 | String family; 106 | 107 | GenusName name; 108 | } 109 | 110 | public static class GenusName { 111 | @Tag(path = "/Animal/genus/name/value") 112 | String value; 113 | } 114 | 115 | public static class AnimalWithPublicSetterFields { 116 | 117 | public AnimalWithPublicSetterFields() { 118 | } 119 | 120 | public boolean typeSetThroughSetter; 121 | 122 | public boolean nameSetThroughSetter; 123 | 124 | @Tag(path = "/Animal/type") 125 | private String type; 126 | 127 | @Tag(path = "/Animal/name") 128 | private String name; 129 | 130 | public String getType() { 131 | return type; 132 | } 133 | 134 | public void setType(String type) { 135 | this.typeSetThroughSetter = true; 136 | this.type = type; 137 | } 138 | 139 | public String getName() { 140 | return name; 141 | } 142 | 143 | public void setName(String name) { 144 | this.nameSetThroughSetter = true; 145 | this.name = name; 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/DeserializationInstanceCreationTest.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize; 2 | 3 | import org.assertj.core.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import io.jonasg.xjx.serdes.Tag; 7 | import io.jonasg.xjx.serdes.XjxSerdes; 8 | 9 | public class DeserializationInstanceCreationTest { 10 | 11 | @Test 12 | void instantiateUsingDefaultConstructor() { 13 | // given 14 | String data = """ 15 | 16 | 17 | dog 18 | John 19 | 20 | """; 21 | 22 | // when 23 | AnimalWithPublicFields animal = new XjxSerdes().read(data, AnimalWithPublicFields.class); 24 | 25 | // then 26 | Assertions.assertThat(animal.type).isEqualTo("dog"); 27 | Assertions.assertThat(animal.name).isEqualTo("John"); 28 | } 29 | 30 | static class AnimalWithPublicFields { 31 | @Tag(path = "/Animal/type") 32 | String type; 33 | @Tag(path = "/Animal/name") 34 | String name; 35 | } 36 | 37 | @Test 38 | void instantiateUsingPrivateConstructor() { 39 | // given 40 | String data = """ 41 | 42 | 43 | dog 44 | John 45 | 46 | """; 47 | 48 | // when 49 | ClassWithPrivateConstructor animal = new XjxSerdes().read(data, ClassWithPrivateConstructor.class); 50 | 51 | // then 52 | Assertions.assertThat(animal.type).isEqualTo("dog"); 53 | Assertions.assertThat(animal.name).isEqualTo("John"); 54 | } 55 | 56 | static class ClassWithPrivateConstructor { 57 | @Tag(path = "/Animal/type") 58 | String type; 59 | @Tag(path = "/Animal/name") 60 | String name; 61 | 62 | private ClassWithPrivateConstructor() { 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/EnumDeserializationTest.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize; 2 | 3 | import org.assertj.core.api.ThrowableAssert.ThrowingCallable; 4 | import org.junit.jupiter.api.Nested; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import io.jonasg.xjx.serdes.Tag; 8 | import io.jonasg.xjx.serdes.XjxSerdes; 9 | import io.jonasg.xjx.serdes.deserialize.CustomValueDeserializationTest.EnumHolder; 10 | import io.jonasg.xjx.serdes.deserialize.CustomValueDeserializationTest.Unit; 11 | import static org.assertj.core.api.Assertions.*; 12 | 13 | public class EnumDeserializationTest { 14 | 15 | 16 | @Test 17 | void mapToFieldOfTypeEnum_whenCharacterDataMatchesEnumConstantName() { 18 | // given 19 | String data = """ 20 | 21 | 22 | 23 | 24 | 78 25 | FAHRENHEIT 26 | 27 | 28 | 62 29 | FAHRENHEIT 30 | 31 | 32 | 10 33 | PERCENTAGE 34 | 35 | Partly Cloudy 36 | 37 | 38 | """; 39 | 40 | // when 41 | EnumHolder enumHolder = new XjxSerdes().read(data, EnumHolder.class); 42 | 43 | // then 44 | assertThat(enumHolder.dayHighUnit).isEqualTo(Unit.FAHRENHEIT); 45 | assertThat(enumHolder.precipitationUnit).isEqualTo(Unit.PERCENTAGE); 46 | } 47 | 48 | @Test 49 | void defaultToNullWhenValueCannotBeMappedToName() { 50 | // given 51 | String data = """ 52 | 53 | 54 | 55 | 56 | 78 57 | fahrenheit 58 | 59 | 60 | 62 61 | fahrenheit 62 | 63 | 64 | 10 65 | percentage 66 | 67 | Partly Cloudy 68 | 69 | 70 | """; 71 | 72 | // when 73 | EnumHolder enumHolder = new XjxSerdes().read(data, EnumHolder.class); 74 | 75 | // then 76 | assertThat(enumHolder.dayHighUnit).isNull(); 77 | assertThat(enumHolder.precipitationUnit).isNull(); 78 | } 79 | 80 | @Nested 81 | class ConfigurationTest { 82 | 83 | @Test 84 | void defaultToNullWhenValueCannotBeMappedToName() { 85 | // given 86 | String data = """ 87 | 88 | 89 | 90 | 91 | 78 92 | fahrenheit 93 | 94 | 95 | 62 96 | fahrenheit 97 | 98 | 99 | 10 100 | percentage 101 | 102 | Partly Cloudy 103 | 104 | 105 | """; 106 | 107 | // when 108 | EnumHolder enumHolder = new XjxSerdes(c -> c.failOnUnknownEnumValue(false)) 109 | .read(data, EnumHolder.class); 110 | 111 | // then 112 | assertThat(enumHolder.dayHighUnit).isNull(); 113 | assertThat(enumHolder.precipitationUnit).isNull(); 114 | } 115 | 116 | @Test 117 | void failOnUnmappableEnumValue() { 118 | // given 119 | String data = """ 120 | 121 | 122 | 123 | 124 | 78 125 | unmappable 126 | 127 | 128 | 62 129 | unmappable 130 | 131 | 132 | 10 133 | fahrenheit/Unit> 134 | 135 | Partly Cloudy 136 | 137 | 138 | """; 139 | 140 | // when 141 | ThrowingCallable throwingCallable = () -> new XjxSerdes(c -> c.failOnUnknownEnumValue(true)) 142 | .read(data, EnumHolder.class); 143 | 144 | // then 145 | assertThatThrownBy(throwingCallable) 146 | .isInstanceOf(XjxDeserializationException.class) 147 | .hasMessageContaining("Cannot map value 'unmappable' to enum Unit"); 148 | } 149 | } 150 | 151 | static class EnumHolder { 152 | @Tag(path = "/WeatherData/Day/High/Unit") 153 | Unit dayHighUnit; 154 | 155 | @Tag(path = "/WeatherData/Day/Precipitation/Unit") 156 | Unit precipitationUnit; 157 | } 158 | 159 | enum Unit { 160 | FAHRENHEIT, PERCENTAGE 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/GeneralDeserializationTest.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 5 | 6 | import org.assertj.core.api.ThrowableAssert; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import io.jonasg.xjx.serdes.Tag; 10 | import io.jonasg.xjx.serdes.XjxSerdes; 11 | 12 | public class GeneralDeserializationTest { 13 | 14 | @Test 15 | void ignoreUnmappedFieldsAndLeaveThemUninitialized() { 16 | // given 17 | String data = """ 18 | 19 | 20 | 5.7 21 | 22 | """; 23 | 24 | // when 25 | UnMappedDataHolder unMappedDataHolder = new XjxSerdes().read(data, UnMappedDataHolder.class); 26 | 27 | // then 28 | assertThat(unMappedDataHolder.aDouble).isEqualTo(Double.valueOf(5.7)); 29 | assertThat(unMappedDataHolder.unmappedNull).isNull(); 30 | assertThat(unMappedDataHolder.unmappedInitialized).isEqualTo("initialized"); 31 | } 32 | 33 | static class UnMappedDataHolder { 34 | @Tag(path = "/DataTypes/Double") 35 | Double aDouble; 36 | 37 | String unmappedNull; 38 | 39 | String unmappedInitialized = "initialized"; 40 | } 41 | 42 | @Test 43 | void nestedComplexTypesDoNotNeedTopLevelMapping_WhenInnerMappingsContainAbsolutePaths() { 44 | // given 45 | String data = """ 46 | 47 | 48 | 5.7 49 | 50 | """; 51 | 52 | // when 53 | NestedComplexTypeWithoutMappingDataHolder dataHolder = new XjxSerdes().read(data, NestedComplexTypeWithoutMappingDataHolder.class); 54 | 55 | // then 56 | assertThat(dataHolder.nestedComplexType.aDouble).isEqualTo(Double.valueOf(5.7)); 57 | } 58 | 59 | static class NestedComplexTypeWithoutMappingDataHolder { 60 | NestedComplexType nestedComplexType; 61 | } 62 | 63 | static class NestedComplexType { 64 | @Tag(path = "/DataTypes/Double") 65 | Double aDouble; 66 | } 67 | 68 | @Test 69 | void mapRelativePaths() { 70 | // given 71 | String data = """ 72 | 73 | 74 | 75 | 76 | 77 | 78 78 | celcius 79 | 80 | 81 | 62 82 | fahrenheit 83 | 84 | 85 | 10 86 | percentage 87 | 88 | Partly Cloudy 89 | 90 | 91 | 92 | """; 93 | 94 | // when 95 | RelativeMappedParentDataHolder dataHolder = new XjxSerdes().read(data, RelativeMappedParentDataHolder.class); 96 | 97 | // then 98 | assertThat(dataHolder.week.day.highValue).isEqualTo(Double.valueOf(78)); 99 | } 100 | 101 | static class RelativeMappedParentDataHolder { 102 | @Tag(path = "/WeatherData/Week") 103 | Week week; 104 | } 105 | 106 | static class Week { 107 | @Tag(path = "Day") 108 | Day day; 109 | } 110 | 111 | static class Day { 112 | @Tag(path = "High/Value") 113 | Double highValue; 114 | } 115 | 116 | @Test 117 | void warnUserWHenRelativePathIsUsed_butParentsDoNotResolveToAbsolutePath() { 118 | // given 119 | String data = """ 120 | 121 | 122 | 123 | 124 | 125 | 78 126 | celcius 127 | 128 | 129 | 62 130 | fahrenheit 131 | 132 | 133 | 10 134 | percentage 135 | 136 | Partly Cloudy 137 | 138 | 139 | 140 | """; 141 | 142 | // when 143 | ThrowableAssert.ThrowingCallable deserializing = () -> new XjxSerdes().read(data, RelativeMappedWithoutParentTagDataHolder.class); 144 | 145 | // then 146 | assertThatThrownBy(deserializing) 147 | .isInstanceOf(XjxDeserializationException.class) 148 | .hasMessage("Field day is annotated with @Tag but one of it's parent is missing a @Tag."); 149 | } 150 | 151 | static class RelativeMappedWithoutParentTagDataHolder { 152 | Week week; 153 | } 154 | 155 | @Test 156 | void fieldsNotAnnotatedWithTagShouldBeIgnored() { 157 | // given 158 | String data = """ 159 | 160 | 161 | 5.7 162 | 163 | """; 164 | 165 | // when 166 | var holder = new XjxSerdes().read(data, FieldsWithoutTagAnnotationHolder.class); 167 | 168 | // then 169 | assertThat(holder.Double).isNull(); 170 | } 171 | 172 | static class FieldsWithoutTagAnnotationHolder { 173 | Double Double; 174 | } 175 | 176 | @Test 177 | void ignorePathMappingsEndingWithSlash() { 178 | // given 179 | String data = """ 180 | 181 | 182 | 5.7 183 | 184 | """; 185 | 186 | // when 187 | var holder = new XjxSerdes().read(data, SlashSuffixedHolder.class); 188 | 189 | // then 190 | assertThat(holder.Double).isEqualTo(5.7D); 191 | } 192 | 193 | static class SlashSuffixedHolder { 194 | @Tag(path = "/DataTypes/Double/") 195 | Double Double; 196 | } 197 | 198 | @Test 199 | void ignoreSuffixedAndPrefixedWhiteSpaceInPathMappings() { 200 | // given 201 | String data = """ 202 | 203 | 204 | 5.7 205 | 206 | """; 207 | 208 | // when 209 | var holder = new XjxSerdes().read(data, WhiteSpacePathMappingHolder.class); 210 | 211 | // then 212 | assertThat(holder.Double).isEqualTo(5.7D); 213 | } 214 | 215 | static class WhiteSpacePathMappingHolder { 216 | @Tag(path = " /DataTypes/Double ") 217 | Double Double; 218 | } 219 | 220 | @Test 221 | void absoluteRootMappingWithTopLevelMappedRootType() { 222 | // given 223 | String data = """ 224 | 225 | 226 | 5.7 227 | 228 | """; 229 | 230 | // when 231 | var holder = new XjxSerdes().read(data, AbsoluteRootMappingHolder.class); 232 | 233 | // then 234 | assertThat(holder.Double).isEqualTo(5.7D); 235 | } 236 | 237 | @Tag(path = "/DataTypes") 238 | static class AbsoluteRootMappingHolder { 239 | @Tag(path = "/DataTypes/Double") 240 | Double Double; 241 | } 242 | 243 | @Test 244 | void namespaceSupport() { 245 | // given 246 | String data = """ 247 | 248 | 249 | 5.7 250 | TableB 251 | 252 | """; 253 | 254 | // when 255 | var holder = new XjxSerdes().read(data, NamespaceHolder.class); 256 | 257 | // then 258 | assertThat(holder.tableA).isEqualTo(5.7D); 259 | assertThat(holder.tableB).isEqualTo("TableB"); 260 | } 261 | 262 | static class NamespaceHolder { 263 | @Tag(path = "/Tables/TableA") 264 | Double tableA; 265 | 266 | @Tag(path = "/Tables/TableB") 267 | String tableB; 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /xjx-serdes/src/test/java/io/jonasg/xjx/serdes/deserialize/TagAttributeDeserializationTest.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.deserialize; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.math.BigDecimal; 6 | 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.params.ParameterizedTest; 9 | import org.junit.jupiter.params.provider.ValueSource; 10 | 11 | import io.jonasg.xjx.serdes.Tag; 12 | import io.jonasg.xjx.serdes.XjxSerdes; 13 | 14 | public class TagAttributeDeserializationTest { 15 | @Test 16 | void deserialize_StringField() { 17 | // given 18 | String data = """ 19 | 20 | 21 | 22 | 23 | """; 24 | 25 | // when 26 | DataTypeHolder dataTypes = new XjxSerdes().read(data, DataTypeHolder.class); 27 | 28 | // then 29 | assertThat(dataTypes.String).isEqualTo("11"); 30 | } 31 | 32 | @Test 33 | void deserialize_IntegerField() { 34 | // given 35 | String data = """ 36 | 37 | 38 | 39 | 40 | """; 41 | 42 | // when 43 | DataTypeHolder dataTypes = new XjxSerdes().read(data, DataTypeHolder.class); 44 | 45 | // then 46 | assertThat(dataTypes.Integer).isEqualTo(11); 47 | } 48 | 49 | 50 | @Test 51 | void deserialize_primitiveIntField() { 52 | // given 53 | String data = """ 54 | 55 | 56 | 57 | 58 | """; 59 | 60 | // when 61 | DataTypeHolder dataTypes = new XjxSerdes().read(data, DataTypeHolder.class); 62 | 63 | // then 64 | assertThat(dataTypes.primitiveInt).isEqualTo(11); 65 | } 66 | 67 | @Test 68 | void deserialize_LongField() { 69 | // given 70 | String data = """ 71 | 72 | 73 | 74 | 75 | """; 76 | 77 | // when 78 | DataTypeHolder dataTypes = new XjxSerdes().read(data, DataTypeHolder.class); 79 | 80 | // then 81 | assertThat(dataTypes.Long).isEqualTo(11L); 82 | } 83 | 84 | @Test 85 | void deserialize_primitiveLongField() { 86 | // given 87 | String data = """ 88 | 89 | 90 | 91 | 92 | """; 93 | 94 | // when 95 | DataTypeHolder dataTypes = new XjxSerdes().read(data, DataTypeHolder.class); 96 | 97 | // then 98 | assertThat(dataTypes.primitiveLong).isEqualTo(11L); 99 | } 100 | 101 | @Test 102 | void deserialize_BigDecimalField() { 103 | // given 104 | String data = """ 105 | 106 | 107 | 108 | 109 | """; 110 | 111 | // when 112 | DataTypeHolder dataTypes = new XjxSerdes().read(data, DataTypeHolder.class); 113 | 114 | // then 115 | assertThat(dataTypes.BigDecimal).isEqualTo(BigDecimal.valueOf(11)); 116 | } 117 | 118 | @Test 119 | void deserialize_DoubleField() { 120 | // given 121 | String data = """ 122 | 123 | 124 | 125 | 126 | """; 127 | 128 | // when 129 | DataTypeHolder dataTypes = new XjxSerdes().read(data, DataTypeHolder.class); 130 | 131 | // then 132 | assertThat(dataTypes.Double).isEqualTo(Double.valueOf(11)); 133 | } 134 | 135 | @Test 136 | void deserialize_primitiveDoubleField() { 137 | // given 138 | String data = """ 139 | 140 | 141 | 142 | 143 | """; 144 | 145 | // when 146 | DataTypeHolder dataTypes = new XjxSerdes().read(data, DataTypeHolder.class); 147 | 148 | // then 149 | assertThat(dataTypes.primitiveDouble).isEqualTo(Double.valueOf(11)); 150 | } 151 | 152 | @Test 153 | void deserialize_multiCharString_toPrimitiveCharField() { 154 | // given 155 | String data = """ 156 | 157 | 158 | 159 | 160 | """; 161 | 162 | // when 163 | DataTypeHolder dataTypes = new XjxSerdes().read(data, DataTypeHolder.class); 164 | 165 | // then 166 | assertThat(dataTypes.multipleCharacters).isEqualTo('A'); 167 | } 168 | 169 | @Test 170 | void deserialize_primitiveCharField() { 171 | // given 172 | String data = """ 173 | 174 | 175 | 176 | 177 | """; 178 | 179 | // when 180 | DataTypeHolder dataTypes = new XjxSerdes().read(data, DataTypeHolder.class); 181 | 182 | // then 183 | assertThat(dataTypes.primitiveChar).isEqualTo('A'); 184 | } 185 | 186 | @Test 187 | void deserialize_CharacterField() { 188 | // given 189 | String data = """ 190 | 191 | 192 | 193 | 194 | """; 195 | 196 | // when 197 | DataTypeHolder dataTypes = new XjxSerdes().read(data, DataTypeHolder.class); 198 | 199 | // then 200 | assertThat(dataTypes.Character).isEqualTo(Character.valueOf('A')); 201 | } 202 | 203 | @ParameterizedTest 204 | @ValueSource(strings = {"True","true","1","yes","YeS"}) 205 | void deserializeTrueValuesFor_booleanField(String value) { 206 | // given 207 | String data = """ 208 | 209 | 210 | 211 | 212 | """.formatted(value); 213 | // when 214 | DataTypeHolder dataTypes = new XjxSerdes().read(data, DataTypeHolder.class); 215 | 216 | // then 217 | assertThat(dataTypes.BooleanTrue).isTrue(); 218 | assertThat(dataTypes.booleanTrue).isTrue(); 219 | } 220 | 221 | @ParameterizedTest 222 | @ValueSource(strings = {"False","false","0","no","No"}) 223 | void deserializeFalseValuesFor_booleanField(String value) { 224 | // given 225 | String data = """ 226 | 227 | 228 | 229 | 230 | """.formatted(value); 231 | // when 232 | DataTypeHolder dataTypes = new XjxSerdes().read(data, DataTypeHolder.class); 233 | 234 | // then 235 | assertThat(dataTypes.BooleanFalse).isFalse(); 236 | assertThat(dataTypes.booleanFalse).isFalse(); 237 | } 238 | 239 | @Test 240 | void deserialize_StringField_mappedUsingRelativePath() { 241 | // given 242 | String data = """ 243 | 244 | 245 | 246 | 247 | """; 248 | 249 | // when 250 | ParentHolder parentHolder = new XjxSerdes().read(data, ParentHolder.class); 251 | 252 | // then 253 | assertThat(parentHolder.nestedField.String).isEqualTo("11"); 254 | } 255 | 256 | @Test 257 | void deserialize_mapValueAndAtTheSameTimeAttributeValuesOfOneTag() { 258 | // given 259 | String data = """ 260 | 261 | 262 | John 263 | 264 | """; 265 | 266 | // when 267 | var person = new XjxSerdes().read(data, Person.class); 268 | 269 | // then 270 | assertThat(person.name).isEqualTo("John"); 271 | assertThat(person.sex).isEqualTo("MALE"); 272 | assertThat(person.age).isEqualTo(18); 273 | } 274 | 275 | public static class Person { 276 | @Tag(path = "/Person/Name") 277 | String name; 278 | 279 | @Tag(path = "/Person/Name", attribute = "sex") 280 | String sex; 281 | 282 | @Tag(path = "/Person/Name", attribute = "age") 283 | int age; 284 | } 285 | 286 | 287 | public static class DataTypeHolder { 288 | public DataTypeHolder() { 289 | } 290 | 291 | @Tag(path = "/DataTypes/String", attribute = "value") 292 | String String; 293 | 294 | @Tag(path = "/DataTypes/Integer", attribute = "value") 295 | Integer Integer; 296 | 297 | @Tag(path = "/DataTypes/primitiveInt", attribute = "value") 298 | int primitiveInt; 299 | 300 | @Tag(path = "/DataTypes/Long", attribute = "value") 301 | Long Long; 302 | 303 | @Tag(path = "/DataTypes/primitiveLong", attribute = "value") 304 | long primitiveLong; 305 | 306 | @Tag(path = "/DataTypes/BigDecimal", attribute = "value") 307 | BigDecimal BigDecimal; 308 | 309 | @Tag(path = "/DataTypes/Double", attribute = "value") 310 | Double Double; 311 | 312 | @Tag(path = "/DataTypes/primitiveDouble", attribute = "value") 313 | double primitiveDouble; 314 | 315 | @Tag(path = "/DataTypes/multipleCharacters", attribute = "value") 316 | char multipleCharacters; 317 | 318 | @Tag(path = "/DataTypes/primitiveChar", attribute = "value") 319 | char primitiveChar; 320 | 321 | @Tag(path = "/DataTypes/Character", attribute = "value") 322 | Character Character; 323 | 324 | @Tag(path = "/DataTypes/BooleanTrue", attribute = "Boolean") 325 | boolean BooleanTrue; 326 | 327 | @Tag(path = "/DataTypes/BooleanTrue", attribute = "boolean") 328 | boolean booleanTrue; 329 | 330 | @Tag(path = "/DataTypes/BooleanFalse", attribute = "Boolean") 331 | boolean BooleanFalse = true; 332 | 333 | @Tag(path = "/DataTypes/BooleanFalse", attribute = "boolean") 334 | boolean booleanFalse = true; 335 | } 336 | 337 | public static class ParentHolder { 338 | @Tag(path = "/DataTypes") 339 | NestedField nestedField; 340 | } 341 | 342 | public static class NestedField { 343 | @Tag(path = "String", attribute = "value") 344 | String String; 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /xjx-serdes/src/test/java/io/jonasg/xjx/serdes/serialize/EnumSerializationTest.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.serialize; 2 | 3 | import org.assertj.core.api.Assertions; 4 | import org.junit.jupiter.api.Nested; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import io.jonasg.xjx.serdes.Tag; 8 | import io.jonasg.xjx.serdes.XjxSerdes; 9 | 10 | public class EnumSerializationTest { 11 | 12 | @Test 13 | void mapToFieldOfTypeEnum_whenCharacterDataMatchesEnumConstantName() { 14 | // given 15 | var enumHolder = new EnumHolder(Unit.FAHRENHEIT, Unit.PERCENTAGE); 16 | 17 | // when 18 | String xml = new XjxSerdes().write(enumHolder); 19 | 20 | // then 21 | Assertions.assertThat(xml).isEqualTo(""" 22 | 23 | 24 | 25 | FAHRENHEIT 26 | 27 | 28 | PERCENTAGE 29 | 30 | 31 | 32 | """); 33 | } 34 | 35 | @Nested 36 | class NullEnumFieldsShouldBeSerializedAsEmptyTag { 37 | 38 | @Test 39 | void withOnlyValue() { 40 | // given 41 | var enumHolder = new EnumHolder(null, Unit.PERCENTAGE); 42 | 43 | // when 44 | String xml = new XjxSerdes().write(enumHolder); 45 | 46 | // then 47 | Assertions.assertThat(xml).isEqualTo(""" 48 | 49 | 50 | 51 | 52 | 53 | 54 | PERCENTAGE 55 | 56 | 57 | 58 | """); 59 | } 60 | 61 | @Test 62 | void withValueAndAttribute() { 63 | // given 64 | var enumHolder = new EnumHolderWithAttr(null, "C", Unit.PERCENTAGE); 65 | 66 | // when 67 | String xml = new XjxSerdes().write(enumHolder); 68 | 69 | // then 70 | Assertions.assertThat(xml).isEqualTo(""" 71 | 72 | 73 | 74 | 75 | 76 | 77 | PERCENTAGE 78 | 79 | 80 | 81 | """); 82 | } 83 | } 84 | 85 | static class EnumHolder { 86 | 87 | public EnumHolder(Unit dayHighUnit, Unit precipitationUnit) { 88 | this.dayHighUnit = dayHighUnit; 89 | this.precipitationUnit = precipitationUnit; 90 | } 91 | 92 | @Tag(path = "/WeatherData/Day/High/Unit") 93 | Unit dayHighUnit; 94 | 95 | @Tag(path = "/WeatherData/Day/Precipitation/Unit") 96 | Unit precipitationUnit; 97 | } 98 | 99 | static class EnumHolderWithAttr { 100 | 101 | public EnumHolderWithAttr(Unit dayHighUnit, String abbreviation, Unit precipitationUnit) { 102 | this.dayHighUnit = dayHighUnit; 103 | this.abbreviation = abbreviation; 104 | this.precipitationUnit = precipitationUnit; 105 | } 106 | 107 | @Tag(path = "/WeatherData/Day/High/Unit") 108 | Unit dayHighUnit; 109 | 110 | @Tag(path = "/WeatherData/Day/High/Unit", attribute = "abbr") 111 | String abbreviation; 112 | 113 | @Tag(path = "/WeatherData/Day/Precipitation/Unit") 114 | Unit precipitationUnit; 115 | } 116 | 117 | enum Unit { 118 | FAHRENHEIT, PERCENTAGE 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /xjx-serdes/src/test/java/io/jonasg/xjx/serdes/serialize/GeneralSerializationTest.java: -------------------------------------------------------------------------------- 1 | package io.jonasg.xjx.serdes.serialize; 2 | 3 | import java.util.stream.Stream; 4 | 5 | import org.assertj.core.api.Assertions; 6 | import org.junit.jupiter.params.ParameterizedTest; 7 | import org.junit.jupiter.params.provider.Arguments; 8 | import org.junit.jupiter.params.provider.MethodSource; 9 | 10 | import io.jonasg.xjx.serdes.Tag; 11 | import io.jonasg.xjx.serdes.XjxSerdes; 12 | 13 | public class GeneralSerializationTest { 14 | 15 | static Stream serializeNestedTags() { 16 | class WeatherDataPojo { 17 | 18 | @Tag(path = "/WeatherData/Location/Country") 19 | private final String country; 20 | 21 | @Tag(path = "/WeatherData/Location/City/Name") 22 | private final String city; 23 | 24 | @Tag(path = "/WeatherData/Location/City/Name", attribute = "code") 25 | private final String code; 26 | 27 | public WeatherDataPojo(String country, String city, String code) { 28 | this.country = country; 29 | this.city = city; 30 | this.code = code; 31 | } 32 | } 33 | record WeatherDataRecord( 34 | @Tag(path = "/WeatherData/Location/Country") 35 | String country, 36 | @Tag(path = "/WeatherData/Location/City/Name") 37 | String city, 38 | @Tag(path = "/WeatherData/Location/City/Name", attribute = "code") 39 | String postalCode 40 | ) {} 41 | return Stream.of( 42 | Arguments.of(new WeatherDataPojo("USA", "New York", "NY")), 43 | Arguments.of(new WeatherDataRecord("USA", "New York", "NY")) 44 | ); 45 | } 46 | 47 | @ParameterizedTest 48 | @MethodSource("serializeNestedTags") 49 | void serializeNestedTags(Object data) { 50 | // when 51 | String xml = new XjxSerdes().write(data); 52 | 53 | // then 54 | Assertions.assertThat(xml).isEqualTo(""" 55 | 56 | 57 | USA 58 | 59 | New York 60 | 61 | 62 | 63 | """); 64 | } 65 | 66 | static Stream serializeNestedObjectsWithoutTagAnnotation() { 67 | class Code { 68 | @Tag(path = "/WeatherData/Location/Country", attribute = "code") 69 | private final String value; 70 | Code(String value) { this.value = value; } 71 | } 72 | class Country { 73 | @Tag(path = "/WeatherData/Location/Country") 74 | private final String name; 75 | private final Code code; 76 | Country(String name, Code code) { this.name = name; this.code = code; } 77 | } 78 | class WeatherData { 79 | private final Country country; 80 | WeatherData(Country country) { this.country = country; } 81 | } 82 | 83 | record CodeRecord(@Tag(path = "/WeatherData/Location/Country", attribute = "code") String value) {} 84 | record CountryRecord(@Tag(path = "/WeatherData/Location/Country") String name, CodeRecord code) {} 85 | record WeatherDataRecord(CountryRecord country) {} 86 | 87 | return Stream.of( 88 | Arguments.of(new WeatherData(new Country("United States of America", new Code("US")))), 89 | Arguments.of(new WeatherDataRecord(new CountryRecord("United States of America", new CodeRecord("US")))) 90 | ); 91 | } 92 | 93 | @ParameterizedTest 94 | @MethodSource("serializeNestedObjectsWithoutTagAnnotation") 95 | void serializeNestedObjectsWithoutTagAnnotation(Object data) { 96 | // when 97 | String xml = new XjxSerdes().write(data); 98 | 99 | // then 100 | Assertions.assertThat(xml).isEqualTo(""" 101 | 102 | 103 | United States of America 104 | 105 | 106 | """); 107 | } 108 | 109 | static Stream serializeNullFieldsToSelfClosingTag() { 110 | class WeatherData { 111 | @Tag(path = "/WeatherData/Location/Country") 112 | private final String country; 113 | @Tag(path = "/WeatherData/Location/City/Name") 114 | private final String city; 115 | WeatherData(String country, String city) { this.country = country; this.city = city; } 116 | } 117 | record WeatherDataRecord(@Tag(path = "/WeatherData/Location/Country") String country, 118 | @Tag(path = "/WeatherData/Location/City/Name") String city) {} 119 | 120 | return Stream.of( 121 | Arguments.of(new WeatherData(null, "New York")), 122 | Arguments.of(new WeatherDataRecord(null, "New York")) 123 | ); 124 | } 125 | 126 | @ParameterizedTest 127 | @MethodSource("serializeNullFieldsToSelfClosingTag") 128 | void serializeNullFieldsToSelfClosingTag(Object data) { 129 | 130 | // when 131 | String xml = new XjxSerdes().write(data); 132 | 133 | // then 134 | Assertions.assertThat(xml).isEqualTo(""" 135 | 136 | 137 | 138 | 139 | New York 140 | 141 | 142 | 143 | """); 144 | } 145 | 146 | static Stream serializeNullFieldsMappedToAnAttribute_byNotAddingAttributeToTag_ofAnAlreadyExistingTag() { 147 | class WeatherData { 148 | @Tag(path = "/WeatherData/Location/Country") 149 | private final String country; 150 | @Tag(path = "/WeatherData/Location/City/Name") 151 | private final String city; 152 | @Tag(path = "/WeatherData/Location/City/Name", attribute = "code") 153 | private final String code; 154 | WeatherData(String country, String city, String code) { this.country = country; this.city = city; this.code = code; } 155 | } 156 | record WeatherDataRecord(@Tag(path = "/WeatherData/Location/Country") String country, 157 | @Tag(path = "/WeatherData/Location/City/Name") String city, 158 | @Tag(path = "/WeatherData/Location/City/Name", attribute = "code") String code) {} 159 | 160 | return Stream.of( 161 | Arguments.of(new WeatherData(null, "New York", null)), 162 | Arguments.of(new WeatherDataRecord(null, "New York", null)) 163 | ); 164 | } 165 | 166 | @ParameterizedTest 167 | @MethodSource("serializeNullFieldsMappedToAnAttribute_byNotAddingAttributeToTag_ofAnAlreadyExistingTag") 168 | void serializeNullFieldsMappedToAnAttribute_byNotAddingAttributeToTag_ofAnAlreadyExistingTag(Object data) { 169 | // when 170 | String xml = new XjxSerdes().write(data); 171 | 172 | // then 173 | Assertions.assertThat(xml).isEqualTo(""" 174 | 175 | 176 | 177 | 178 | New York 179 | 180 | 181 | 182 | """); 183 | } 184 | 185 | static Stream serializeNullFieldsMappedToAnAttribute_byNotAddingAttributeToTag_ofATagThatIsNotMapped() { 186 | class WeatherData { 187 | @Tag(path = "/WeatherData/Location/Country") 188 | private final String country; 189 | @Tag(path = "/WeatherData/Location/City/Name", attribute = "code") 190 | private final String code; 191 | WeatherData(String country, String code) { this.country = country; this.code = code; } 192 | } 193 | record WeatherDataRecord(@Tag(path = "/WeatherData/Location/Country") String country, 194 | @Tag(path = "/WeatherData/Location/City/Name", attribute = "code") String code) {} 195 | 196 | return Stream.of( 197 | Arguments.of(new WeatherData(null, null)), 198 | Arguments.of(new WeatherDataRecord(null, null)) 199 | ); 200 | } 201 | 202 | @ParameterizedTest 203 | @MethodSource("serializeNullFieldsMappedToAnAttribute_byNotAddingAttributeToTag_ofATagThatIsNotMapped") 204 | void serializeNullFieldsMappedToAnAttribute_byNotAddingAttributeToTag_ofATagThatIsNotMapped(Object data) { 205 | // when 206 | String xml = new XjxSerdes().write(data); 207 | 208 | // then 209 | Assertions.assertThat(xml).isEqualTo(""" 210 | 211 | 212 | 213 | 214 | 215 | 216 | """); 217 | } 218 | 219 | static Stream serializeNullFieldsToSelfClosingTagContainingNonNullAttribute() { 220 | class WeatherDataWithAttribute { 221 | @Tag(path = "/WeatherData/Location/Country") 222 | private final String country; 223 | @Tag(path = "/WeatherData/Location/Country", attribute = "code") 224 | private final String code; 225 | 226 | public WeatherDataWithAttribute(String country, String code) { 227 | this.country = country; 228 | this.code = code; 229 | } 230 | } 231 | record WeatherDataWithAttributeRecord( 232 | @Tag(path = "/WeatherData/Location/Country") String country, 233 | @Tag(path = "/WeatherData/Location/Country", attribute = "code") String code 234 | ) {} 235 | 236 | return Stream.of( 237 | Arguments.of(new WeatherDataWithAttribute(null, "USA")), 238 | Arguments.of(new WeatherDataWithAttributeRecord(null, "USA")) 239 | ); 240 | } 241 | 242 | @ParameterizedTest 243 | @MethodSource("serializeNullFieldsToSelfClosingTagContainingNonNullAttribute") 244 | void serializeNullFieldsToSelfClosingTagContainingNonNullAttribute(Object data) { 245 | // when 246 | String xml = new XjxSerdes().write(data); 247 | 248 | // then 249 | Assertions.assertThat(xml).isEqualTo(""" 250 | 251 | 252 | 253 | 254 | 255 | """); 256 | } 257 | } 258 | --------------------------------------------------------------------------------