├── .gitattributes ├── .gitignore ├── .tool-versions ├── .travis.yml ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── project.clj ├── push-javadoc-to-gh-pages.sh ├── scratch.clj ├── settings.gradle.kts ├── src ├── main │ └── java │ │ └── com │ │ └── github │ │ └── rschmitt │ │ └── dynamicobject │ │ ├── Cached.java │ │ ├── DynamicObject.java │ │ ├── DynamicObjectSerializer.java │ │ ├── EdnTranslator.java │ │ ├── FressianReadHandler.java │ │ ├── FressianWriteHandler.java │ │ ├── Key.java │ │ ├── Meta.java │ │ ├── Required.java │ │ ├── Unknown.java │ │ └── internal │ │ ├── ClojureStuff.java │ │ ├── Conversions.java │ │ ├── CustomValidationHook.java │ │ ├── DynamicObjectInstance.java │ │ ├── DynamicObjectPrintHook.java │ │ ├── EdnSerialization.java │ │ ├── EdnTranslatorAdapter.java │ │ ├── FressianSerialization.java │ │ ├── Instances.java │ │ ├── InvokeDynamicInvocationHandler.java │ │ ├── Numerics.java │ │ ├── Primitives.java │ │ ├── RecordReader.java │ │ ├── Reflection.java │ │ ├── Serialization.java │ │ ├── Validation.java │ │ └── indyproxy │ │ ├── ConcreteMethodTracker.java │ │ ├── DefaultInvocationHandler.java │ │ ├── DynamicInvocationHandler.java │ │ ├── DynamicProxy.java │ │ └── MethodIdentifier.java └── test │ └── java │ └── com │ └── github │ └── rschmitt │ └── dynamicobject │ ├── AcceptanceTest.java │ ├── BuilderTest.java │ ├── CollectionsTest.java │ ├── ColliderTest.java │ ├── CustomKeyTest.java │ ├── CustomMethodTest.java │ ├── DefaultReaderTest.java │ ├── DeserializationHookTest.java │ ├── DiffTest.java │ ├── ExtensibilityTest.java │ ├── FressianTest.java │ ├── InstantTest.java │ ├── MapTest.java │ ├── MergeTest.java │ ├── MetadataTest.java │ ├── MethodHandleTest.java │ ├── NestingTest.java │ ├── NumberTest.java │ ├── ObjectMethodsTest.java │ ├── OptionalTest.java │ ├── PrimitiveTest.java │ ├── PrintingTest.java │ ├── RecordTest.java │ ├── RecursionTest.java │ ├── SchemaCollectionTest.java │ ├── StaticFieldTest.java │ ├── StreamingTest.java │ ├── TestUtils.java │ ├── ValidationTest.java │ ├── benchmark │ ├── DeserializationBenchmark.java │ ├── PrimitiveFieldAccess.java │ └── StringFieldAccess.java │ └── internal │ └── ReflectionTest.java └── test-all-versions.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.sh eol=lf linguist-vendored 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.gradle 2 | /build 3 | /target 4 | /out 5 | /coverage-error.log 6 | /.idea 7 | /*.iml 8 | /.lein* 9 | /.nrepl-port 10 | /bin 11 | .DS_Store 12 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | java corretto-21 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: openjdk8 3 | script: ./test-all-versions.sh 4 | install: ./gradlew build 5 | after_success: ./push-javadoc-to-gh-pages.sh 6 | env: 7 | global: 8 | secure: "FWHVzDfKWfak06QtiuhImMt900A8Dg5oKQaRf7JSTZohmbKZsiYatWcVUUn+uEwXZ/fopaoMR0vYFfCiVacFF7WpnrsP5D3wnvNme2zGdoBdTZSbDu8l3zDfCdCk0S51zWsO5iM7rt799j7mJA7PDu1GvhkNRxZLrY5t0FPn3F+S9DVjUxequau7kWeiMNPucN/wKbC44ul98xQPwXW5IKtdFbchyI0LrftN/wrSzltGQd4Emhud0uYNKGexpE1tS9iuf75wtbjE3v8/xJtMidn2uoPXzeye45dFIEEL7TzeZTIDA2ia2S+Q165XVAv49GkK9lBNx0p1O18fETTfIiWmS4+mpIPsGqPPbIjNIa2ebGgVs3DfSaT7Hs10UcasGjZTGwVI43eRr94ctjW0/yz1E5JDhHCImo0vdQWgV5BPe1CD8R9qLSehI+BoM1lIJimrAevmCvWFT8zT46jR+WqjnF6RtXfijmTLIAHRIV0pa6A/RpHGMG6aXRle+zVjlCc7n548IiWC4k0JcW9WGhNEC/bnxvyTTmTektCQ5EoUtxq0In6JBy12QyRjY31tyu8SoqCLwlo1sSr/FWPVunI57GJAy2kL6zbVmk4Gpp7FRevjhUakZqWARd2z/T+nHHtYiusTBuzrn+l9Y/kr86xubPT7csoMxAVY9X9UZ4s=" 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | 3 | Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an "owner") of an original work of 8 | authorship and/or a database (each, a "Work"). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific 12 | works ("Commons") that the public can reliably and without fear of later 13 | claims of infringement build upon, modify, incorporate in other works, reuse 14 | and redistribute as freely as possible in any form whatsoever and for any 15 | purposes, including without limitation commercial purposes. These owners may 16 | contribute to the Commons to promote the ideal of a free culture and the 17 | further production of creative, cultural and scientific works, or to gain 18 | reputation or greater distribution for their Work in part through the use and 19 | efforts of others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation 22 | of additional consideration or compensation, the person associating CC0 with a 23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 25 | and publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights ("Copyright and 31 | Related Rights"). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 34 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 35 | and translate a Work; 36 | 37 | ii. moral rights retained by the original author(s) and/or performer(s); 38 | 39 | iii. publicity and privacy rights pertaining to a person's image or likeness 40 | depicted in a Work; 41 | 42 | iv. rights protecting against unfair competition in regards to a Work, 43 | subject to the limitations in paragraph 4(a), below; 44 | 45 | v. rights protecting the extraction, dissemination, use and reuse of data in 46 | a Work; 47 | 48 | vi. database rights (such as those arising under Directive 96/9/EC of the 49 | European Parliament and of the Council of 11 March 1996 on the legal 50 | protection of databases, and under any national implementation thereof, 51 | including any amended or successor version of such directive); and 52 | 53 | vii. other similar, equivalent or corresponding rights throughout the world 54 | based on applicable law or treaty, and any national implementations thereof. 55 | 56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 59 | and Related Rights and associated claims and causes of action, whether now 60 | known or unknown (including existing as well as future claims and causes of 61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 62 | duration provided by applicable law or treaty (including future time 63 | extensions), (iii) in any current or future medium and for any number of 64 | copies, and (iv) for any purpose whatsoever, including without limitation 65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes 66 | the Waiver for the benefit of each member of the public at large and to the 67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver 68 | shall not be subject to revocation, rescission, cancellation, termination, or 69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 70 | by the public as contemplated by Affirmer's express Statement of Purpose. 71 | 72 | 3. Public License Fallback. Should any part of the Waiver for any reason be 73 | judged legally invalid or ineffective under applicable law, then the Waiver 74 | shall be preserved to the maximum extent permitted taking into account 75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver 76 | is so judged Affirmer hereby grants to each affected person a royalty-free, 77 | non transferable, non sublicensable, non exclusive, irrevocable and 78 | unconditional license to exercise Affirmer's Copyright and Related Rights in 79 | the Work (i) in all territories worldwide, (ii) for the maximum duration 80 | provided by applicable law or treaty (including future time extensions), (iii) 81 | in any current or future medium and for any number of copies, and (iv) for any 82 | purpose whatsoever, including without limitation commercial, advertising or 83 | promotional purposes (the "License"). The License shall be deemed effective as 84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the 85 | License for any reason be judged legally invalid or ineffective under 86 | applicable law, such partial invalidity or ineffectiveness shall not 87 | invalidate the remainder of the License, and in such case Affirmer hereby 88 | affirms that he or she will not (i) exercise any of his or her remaining 89 | Copyright and Related Rights in the Work or (ii) assert any associated claims 90 | and causes of action with respect to the Work, in either case contrary to 91 | Affirmer's express Statement of Purpose. 92 | 93 | 4. Limitations and Disclaimers. 94 | 95 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 96 | surrendered, licensed or otherwise affected by this document. 97 | 98 | b. Affirmer offers the Work as-is and makes no representations or warranties 99 | of any kind concerning the Work, express, implied, statutory or otherwise, 100 | including without limitation warranties of title, merchantability, fitness 101 | for a particular purpose, non infringement, or the absence of latent or 102 | other defects, accuracy, or the present or absence of errors, whether or not 103 | discoverable, all to the greatest extent permissible under applicable law. 104 | 105 | c. Affirmer disclaims responsibility for clearing rights of other persons 106 | that may apply to the Work or any use thereof, including without limitation 107 | any person's Copyright and Related Rights in the Work. Further, Affirmer 108 | disclaims responsibility for obtaining any necessary consents, permissions 109 | or other rights required for any use of the Work. 110 | 111 | d. Affirmer understands and acknowledges that Creative Commons is not a 112 | party to this document and has no duty or obligation with respect to this 113 | CC0 or use of the Work. 114 | 115 | For more information, please see 116 | 117 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `java-library` 3 | `maven-publish` 4 | signing 5 | } 6 | 7 | group = "com.github.rschmitt" 8 | version = "1.7.3" 9 | 10 | java { 11 | toolchain { 12 | languageVersion = JavaLanguageVersion.of(21) 13 | } 14 | withSourcesJar() 15 | withJavadocJar() 16 | } 17 | 18 | dependencies { 19 | api("org.clojure:clojure:[1.6.0,)") 20 | api("com.github.rschmitt:collider:1.0.0") 21 | api("org.fressian:fressian:0.6.8") 22 | api("org.clojure:data.fressian:1.1.0") 23 | implementation("org.ow2.asm:asm:7.1") 24 | 25 | testCompileOnly("org.junit.jupiter:junit-jupiter-api:5.+") 26 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.+") 27 | testRuntimeOnly("org.junit.platform:junit-platform-launcher") 28 | testImplementation("collection-check:collection-check:0.1.6") 29 | } 30 | 31 | tasks.test { 32 | useJUnitPlatform { 33 | excludeTags("benchmark") 34 | } 35 | } 36 | 37 | tasks.register("benchmark") { 38 | useJUnitPlatform { 39 | includeTags("benchmark") 40 | } 41 | } 42 | 43 | tasks.javadoc { 44 | (options as StandardJavadocDocletOptions).addStringOption("Xdoclint:none", "-quiet") 45 | options.showFromPublic() 46 | exclude("com/github/rschmitt/dynamicobject/internal/**") 47 | } 48 | 49 | val sonatypeUsername: String? by project 50 | val sonatypePassword: String? by project 51 | 52 | repositories { 53 | mavenLocal() 54 | mavenCentral() 55 | maven { 56 | url = uri("https://clojars.org/repo") 57 | } 58 | maven { 59 | url = uri("https://oss.sonatype.org/content/repositories/snapshots") 60 | } 61 | } 62 | 63 | publishing { 64 | repositories { 65 | maven { 66 | url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/") 67 | credentials { 68 | username = sonatypeUsername 69 | password = sonatypePassword 70 | } 71 | } 72 | } 73 | publications { 74 | create("maven") { 75 | from(components["java"]) 76 | pom { 77 | name.set("dynamic-object") 78 | description.set("Lightweight data modeling for Java, powered by Clojure.") 79 | url.set("https://github.com/rschmitt/dynamic-object") 80 | licenses { 81 | license { 82 | name.set("CC0") 83 | url.set("http://creativecommons.org/publicdomain/zero/1.0/") 84 | } 85 | } 86 | developers { 87 | developer { 88 | id.set("rschmitt") 89 | name.set("Ryan Schmitt") 90 | email.set("rschmitt@pobox.com") 91 | } 92 | } 93 | scm { 94 | connection.set("scm:git:git@github.com:rschmitt/dynamic-object.git") 95 | developerConnection.set("scm:git:git@github.com:rschmitt/dynamic-object.git") 96 | url.set("git@github.com:rschmitt/dynamic-object.git") 97 | } 98 | } 99 | } 100 | } 101 | } 102 | 103 | signing { 104 | useGpgCmd() 105 | sign(publishing.publications["maven"]) 106 | } 107 | 108 | tasks.withType(JavaCompile::class) { 109 | options.encoding = "UTF-8" 110 | options.release.set(8) 111 | } 112 | 113 | defaultTasks("build") 114 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.console=verbose 2 | org.gradle.configuration-cache=true 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rschmitt/dynamic-object/2d0e3aafb814dfb3f7dbb8c680ec72c1ac65d89c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject dynamic-object "1.7.3" 2 | :profiles {:dev 3 | {:dependencies 4 | [[org.clojure/clojure "1.7.0"] 5 | [com.github.rschmitt/collider "1.0.0"] 6 | [org.fressian/fressian "0.6.8"] 7 | [org.clojure/data.fressian "1.1.0"] 8 | [junit/junit "4.13"] 9 | [org.ow2.asm/asm "7.1"] 10 | [collection-check "0.1.6"]]}}) 11 | -------------------------------------------------------------------------------- /push-javadoc-to-gh-pages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eux 4 | 5 | if [ "$TRAVIS_REPO_SLUG" == "rschmitt/dynamic-object" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_BRANCH" == "master" ]; then 6 | echo "Generating javadoc..." 7 | ./gradlew javadoc 8 | echo "Publishing javadoc..." 9 | 10 | cp -R build/docs/javadoc $HOME/javadoc-latest 11 | 12 | cd $HOME 13 | git config --global user.email "travis@travis-ci.org" 14 | git config --global user.name "travis-ci" 15 | git clone --quiet --branch=gh-pages https://${GH_TOKEN}@github.com/rschmitt/dynamic-object gh-pages > /dev/null 16 | 17 | cd gh-pages 18 | git rm -rf --ignore-unmatch ./javadoc 19 | cp -Rf $HOME/javadoc-latest ./javadoc 20 | git add -f . 21 | git commit -m "Lastest javadoc on successful travis build $TRAVIS_BUILD_NUMBER auto-pushed to gh-pages" 22 | git push -fq origin gh-pages > /dev/null 23 | 24 | echo "Published Javadoc to gh-pages." 25 | fi 26 | -------------------------------------------------------------------------------- /scratch.clj: -------------------------------------------------------------------------------- 1 | (import 'com.github.rschmitt.dynamicobject.DynamicObject) 2 | (require '[collection-check :refer :all] 3 | '[clojure.test.check.generators :as gen]) 4 | 5 | (def gen-element (gen/tuple gen/int)) 6 | 7 | (assert-map-like 1e3 (DynamicObject/newInstance DynamicObject) gen-element gen-element) 8 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "dynamic-object" 2 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/Cached.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 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 | * Requests that DynamicObject serializers with support for caching or deduplicating repeated values cache the value 10 | * stored under the associated getter or builder. Currently, only the Fressian serializer makes use of this annotation. 11 | *

12 | * The objects stored under this annotation should implement a sensible equals() and hashCode(); additionally, when 13 | * using @Cached, you should be cognizant that the cache used by the serializer may be quite small - Fressian has only 14 | * 32 entries, for instance, and this is shared with cached map keys as well. Therefore, using @Cached on data that 15 | * cannot be effectively cached can greatly negatively impact the size of the encoded data. In particular, using @Cached 16 | * on data that uses default object identity comparison may be a very bad idea. 17 | *

18 | * This annotation does not impact binary compatibility of the serialized data. 19 | * 20 | * @since 1.6.0 21 | */ 22 | @Target({ElementType.METHOD}) 23 | @Retention(RetentionPolicy.RUNTIME) 24 | public @interface Cached { 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/DynamicObject.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import com.github.rschmitt.dynamicobject.internal.EdnSerialization; 4 | import com.github.rschmitt.dynamicobject.internal.FressianSerialization; 5 | import com.github.rschmitt.dynamicobject.internal.Instances; 6 | import com.github.rschmitt.dynamicobject.internal.Serialization; 7 | import org.fressian.FressianReader; 8 | import org.fressian.FressianWriter; 9 | import org.fressian.handlers.ReadHandler; 10 | import org.fressian.handlers.WriteHandler; 11 | 12 | import java.io.InputStream; 13 | import java.io.OutputStream; 14 | import java.io.PushbackReader; 15 | import java.io.Writer; 16 | import java.util.Map; 17 | import java.util.function.BiFunction; 18 | import java.util.stream.Stream; 19 | 20 | @SuppressWarnings("rawtypes") 21 | public interface DynamicObject> extends Map { 22 | /** 23 | * @return the underlying Clojure map backing this instance. Downcasting the return value of this method to any 24 | * particular Java type (e.g. IPersistentMap) is not guaranteed to work with future versions of Clojure. 25 | */ 26 | Map getMap(); 27 | 28 | /** 29 | * @return the apparent type of this instance. Note that {@code getClass} will return the class of the interface 30 | * proxy and not the interface itself. 31 | */ 32 | Class getType(); 33 | 34 | /** 35 | * Invokes clojure.pprint/pprint, which writes a pretty-printed representation of the object to the currently bound 36 | * value of *out*, which defaults to System.out (stdout). 37 | */ 38 | void prettyPrint(); 39 | 40 | /** 41 | * Like {@link DynamicObject#prettyPrint}, but returns the pretty-printed string instead of writing it to *out*. 42 | */ 43 | String toFormattedString(); 44 | 45 | /** 46 | * Return a copy of this instance with {@code other}'s fields merged in (nulls don't count). If a given field is 47 | * present in both instances, the fields in {@code other} will take precedence. 48 | *

49 | * Equivalent to: {@code (merge-with (fn [a b] (if (nil? b) a b)) this other)} 50 | */ 51 | D merge(D other); 52 | 53 | /** 54 | * Recursively compares this instance with {@code other}, returning a new instance containing all of the common 55 | * elements of both {@code this} and {@code other}. Maps and lists are compared recursively; everything else, 56 | * including sets, strings, and POJOs, is treated atomically. 57 | *

58 | * Equivalent to: {@code (nth (clojure.data/diff this other) 2)} 59 | */ 60 | D intersect(D other); 61 | 62 | /** 63 | * Recursively compares this instance with {@code other}, similar to {@link #intersect}, but returning the fields that 64 | * are unique to {@code this}. Uses the same recursion strategy as {@code intersect}. 65 | *

66 | * Equivalent to: {@code (nth (clojure.data/diff this other) 0)} 67 | */ 68 | D subtract(D other); 69 | 70 | /** 71 | * Validate that all fields annotated with @Required are non-null, and that all present fields are of the correct 72 | * type. Returns the validated instance unchanged, which allows the validate method to be called at the end of a 73 | * fluent builder chain. 74 | */ 75 | D validate(); 76 | 77 | /** 78 | * Post-deserialization hook. The intended use of this method is to facilitate format upgrades. For example, if a 79 | * new field has been added to a DynamicObject schema, this method can be implemented to add that field (with 80 | * some appropriate default value) to older data upon deserialization. 81 | *

82 | * If not implemented, this method does nothing. 83 | *

84 | * @deprecated This method is experimental. 85 | */ 86 | @Deprecated 87 | D afterDeserialization(); 88 | 89 | /** 90 | * Serialize the given object to Edn. Any {@code EdnTranslator}s that have been registered through 91 | * {@link DynamicObject#registerType} will be invoked as needed. 92 | */ 93 | static String serialize(Object o) { 94 | return EdnSerialization.serialize(o); 95 | } 96 | 97 | static void serialize(Object o, Writer w) { 98 | EdnSerialization.serialize(o, w); 99 | } 100 | 101 | /** 102 | * Deserializes a DynamicObject or registered type from a String. 103 | * 104 | * @param edn The Edn representation of the object. 105 | * @param type The type of class to deserialize. Must be an interface that extends DynamicObject. 106 | */ 107 | static T deserialize(String edn, Class type) { 108 | return EdnSerialization.deserialize(edn, type); 109 | } 110 | 111 | /** 112 | * Lazily deserialize a stream of top-level Edn elements as the given type. 113 | */ 114 | static Stream deserializeStream(PushbackReader streamReader, Class type) { 115 | return EdnSerialization.deserializeStream(streamReader, type); 116 | } 117 | 118 | /** 119 | * Serialize a single object {@code o} to binary Fressian data. 120 | */ 121 | static byte[] toFressianByteArray(Object o) { 122 | return FressianSerialization.toFressianByteArray(o); 123 | } 124 | 125 | /** 126 | * Deserialize and return the Fressian-encoded object in {@code bytes}. 127 | */ 128 | static T fromFressianByteArray(byte[] bytes) { 129 | return FressianSerialization.fromFressianByteArray(bytes); 130 | } 131 | 132 | /** 133 | * Create a {@link FressianReader} instance to read from {@code is}. The reader will be created with support for all 134 | * the basic Java and Clojure types, all DynamicObject types registered by calling {@link #registerTag(Class, 135 | * String)}, and any other types registered by calling {@link #registerType(Class, String, ReadHandler, 136 | * WriteHandler)}. If {@code validateChecksum} is true, the data will be checksummed as it is read; this checksum 137 | * can later be compared to the expected checksum in the Fressian footer by calling {@link 138 | * FressianReader#validateFooter()}. 139 | */ 140 | static FressianReader createFressianReader(InputStream is, boolean validateChecksum) { 141 | return FressianSerialization.createFressianReader(is, validateChecksum); 142 | } 143 | 144 | /** 145 | * Create a {@link FressianWriter} instance to write to {@code os}. The writer will be created with support for all 146 | * the basic Java and Clojure types, all DynamicObject types registered by calling {@link #registerTag(Class, 147 | * String)}, and any other types registered by calling {@link #registerType(Class, String, ReadHandler, 148 | * WriteHandler)}. If desired, a Fressian footer (containing an Adler32 checksum of all data written) can be written 149 | * by calling {@link FressianWriter#writeFooter()}. 150 | */ 151 | static FressianWriter createFressianWriter(OutputStream os) { 152 | return FressianSerialization.createFressianWriter(os); 153 | } 154 | 155 | /** 156 | * Lazily deserialize a stream of Fressian-encoded values as the given type. A Fressian footer, if encountered, will 157 | * be validated. 158 | */ 159 | static Stream deserializeFressianStream(InputStream is, Class type) { 160 | return FressianSerialization.deserializeFressianStream(is, type); 161 | } 162 | 163 | /** 164 | * Use the supplied {@code map} to back an instance of {@code type}. 165 | */ 166 | static > D wrap(Map map, Class type) { 167 | return Instances.wrap(map, type); 168 | } 169 | 170 | /** 171 | * Create a "blank" instance of {@code type}, backed by an empty Clojure map. All fields will be null. 172 | */ 173 | static > D newInstance(Class type) { 174 | return Instances.newInstance(type); 175 | } 176 | 177 | /** 178 | * Register an {@link EdnTranslator} to enable instances of {@code type} to be serialized to and deserialized from 179 | * Edn using reader tags. 180 | */ 181 | static void registerType(Class type, EdnTranslator translator) { 182 | EdnSerialization.registerType(type, translator); 183 | } 184 | 185 | /** 186 | * Register a {@link org.fressian.handlers.ReadHandler} and {@link org.fressian.handlers.WriteHandler} to enable 187 | * instances of {@code type} to be serialized to and deserialized from Fressian data. 188 | */ 189 | static void registerType(Class type, String tag, ReadHandler readHandler, WriteHandler writeHandler) { 190 | FressianSerialization.registerType(type, tag, readHandler, writeHandler); 191 | } 192 | 193 | /** 194 | * Deregister the given {@code translator}. After this method is invoked, it will no longer be possible to read or 195 | * write instances of {@code type} unless another translator is registered. 196 | */ 197 | static void deregisterType(Class type) { 198 | Serialization.deregisterType(type); 199 | } 200 | 201 | /** 202 | * Register a reader tag for a DynamicObject type. This is useful for reading Edn representations of Clojure 203 | * records. 204 | */ 205 | static > void registerTag(Class type, String tag) { 206 | Serialization.registerTag(type, tag); 207 | } 208 | 209 | /** 210 | * Deregister the reader tag for the given DynamicObject type. 211 | */ 212 | static > void deregisterTag(Class type) { 213 | Serialization.deregisterTag(type); 214 | } 215 | 216 | /** 217 | * Specify a default reader, which is a function that will be called when any unknown reader tags are encountered. 218 | * The function will be passed the reader tag (as a string) and the tagged Edn element, and can return whatever it 219 | * wants. 220 | *

221 | * DynamicObject comes with a built-in default reader for unknown elements, which returns an instance of {@link 222 | * com.github.rschmitt.dynamicobject.Unknown}, which simply captures the (tag, element) tuple from the Edn reader. 223 | * The {@code Unknown} class is handled specially during serialization so that unknown elements can be serialized 224 | * correctly; this allows unknown types to be passed through transparently. 225 | *

226 | * To disable the default reader, call {@code DynamicObject.setDefaultReader(null)}. This will cause the reader to 227 | * throw an exception if unknown reader tags are encountered. 228 | */ 229 | static void setDefaultReader(BiFunction reader) { 230 | EdnSerialization.setDefaultReader(reader); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/DynamicObjectSerializer.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import com.github.rschmitt.dynamicobject.internal.EdnSerialization; 4 | import com.github.rschmitt.dynamicobject.internal.FressianSerialization; 5 | 6 | import org.fressian.FressianReader; 7 | import org.fressian.FressianWriter; 8 | 9 | import java.io.InputStream; 10 | import java.io.OutputStream; 11 | import java.io.PushbackReader; 12 | import java.io.Writer; 13 | import java.util.stream.Stream; 14 | 15 | /** 16 | * A utility class for DynamicObject (de)serialization. All of the methods in this class delegate 17 | * directly to the static methods in {@linkplain DynamicObject}. The difference is that this class 18 | * is instantiable, and can therefore participate in dependency injection. This makes it 19 | * straightforward to ensure that types and serialization tags are registered with DynamicObject 20 | * before any serialization is attempted. 21 | *

22 | * For example, if you are using Guice, you can write 23 | * a {@code DynamicObjectSerializer} provider method that registers types:

 24 |  * @Provides
 25 |  * @Singleton
 26 |  * DynamicObjectSerializer getDynamicObjectSerializer() {
 27 |  *     DynamicObject.registerTag(Record.class, "recordtag");
 28 |  *     DynamicObject.registerType(Identifier.class, new IdentifierTranslator());
 29 |  *     return new DynamicObjectSerializer();
 30 |  * }
 31 |  * 
32 | * Classes that need to perform serialization can then have a {@code DynamicObjectSerializer} 33 | * injected at construction time:
 34 |  * private final DynamicObjectSerializer serializer;
 35 |  *
 36 |  * @Inject
 37 |  * public FlatFileWriter(DynamicObjectSerializer serializer) {
 38 |  *     this.serializer = serializer;
 39 |  * }
 40 |  *
 41 |  * public void persist(Record rec) throws IOException {
 42 |  *     File file = new File("record.txt");
 43 |  *     try (
 44 |  *         OutputStream os = new BufferedOutputStream(new FileOutputStream(file));
 45 |  *         Writer w = new OutputStreamWriter(os, StandardCharsets.UTF_8)
 46 |  *     ) {
 47 |  *         serializer.serialize(rec, w);
 48 |  *     }
 49 |  * }
 50 |  * 
51 | */ 52 | public class DynamicObjectSerializer { 53 | /** 54 | * @see DynamicObject#serialize(Object) 55 | */ 56 | public String serialize(Object o) { 57 | return EdnSerialization.serialize(o); 58 | } 59 | 60 | /** 61 | * @see DynamicObject#serialize(Object, Writer) 62 | */ 63 | public void serialize(Object o, Writer w) { 64 | EdnSerialization.serialize(o, w); 65 | } 66 | 67 | /** 68 | * @see DynamicObject#deserialize(String, Class) 69 | */ 70 | public T deserialize(String edn, Class type) { 71 | return EdnSerialization.deserialize(edn, type); 72 | } 73 | 74 | /** 75 | * @see DynamicObject#deserializeStream(PushbackReader, Class) 76 | */ 77 | public Stream deserializeStream(PushbackReader streamReader, Class type) { 78 | return EdnSerialization.deserializeStream(streamReader, type); 79 | } 80 | 81 | /** 82 | * @see DynamicObject#toFressianByteArray(Object) 83 | */ 84 | public byte[] toFressianByteArray(Object o) { 85 | return FressianSerialization.toFressianByteArray(o); 86 | } 87 | 88 | /** 89 | * @see DynamicObject#fromFressianByteArray(byte[]) 90 | */ 91 | public T fromFressianByteArray(byte[] bytes) { 92 | return FressianSerialization.fromFressianByteArray(bytes); 93 | } 94 | 95 | /** 96 | * @see DynamicObject#createFressianReader(InputStream, boolean) 97 | */ 98 | public FressianReader createFressianReader(InputStream is, boolean validateChecksum) { 99 | return FressianSerialization.createFressianReader(is, validateChecksum); 100 | } 101 | 102 | /** 103 | * @see DynamicObject#createFressianWriter(OutputStream) 104 | */ 105 | public FressianWriter createFressianWriter(OutputStream os) { 106 | return FressianSerialization.createFressianWriter(os); 107 | } 108 | 109 | /** 110 | * @see DynamicObject#deserializeFressianStream(InputStream, Class) 111 | */ 112 | public Stream deserializeFressianStream(InputStream is, Class type) { 113 | return FressianSerialization.deserializeFressianStream(is, type); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/EdnTranslator.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import java.io.IOException; 4 | import java.io.StringWriter; 5 | import java.io.Writer; 6 | 7 | public interface EdnTranslator { 8 | /** 9 | * Read a tagged Edn object as its intended type. 10 | */ 11 | T read(Object obj); 12 | 13 | /** 14 | * Return an Edn representation of the given object. 15 | */ 16 | default String write(T obj) { 17 | StringWriter stringWriter = new StringWriter(); 18 | write(obj, stringWriter); 19 | return stringWriter.toString(); 20 | } 21 | 22 | /** 23 | * Return the tag literal to use during serialization. 24 | */ 25 | String getTag(); 26 | 27 | /** 28 | * Write an Edn representation of the given object to the given Writer. 29 | */ 30 | default void write(T obj, Writer writer) { 31 | try { 32 | writer.write(write(obj)); 33 | } catch (IOException ex) { 34 | throw new RuntimeException(ex); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/FressianReadHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import org.fressian.Reader; 4 | import org.fressian.handlers.ReadHandler; 5 | 6 | import java.io.IOException; 7 | import java.util.Map; 8 | 9 | public class FressianReadHandler> implements ReadHandler { 10 | private final Class type; 11 | 12 | public FressianReadHandler(Class type) { 13 | this.type = type; 14 | } 15 | 16 | @Override 17 | @SuppressWarnings("deprecation") 18 | public Object read(Reader r, Object tag, int componentCount) throws IOException { 19 | return DynamicObject.wrap((Map) r.readObject(), type).afterDeserialization(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/FressianWriteHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import org.fressian.CachedObject; 4 | import org.fressian.Writer; 5 | import org.fressian.handlers.WriteHandler; 6 | 7 | import javax.annotation.concurrent.Immutable; 8 | import javax.annotation.concurrent.NotThreadSafe; 9 | import java.io.IOException; 10 | import java.util.AbstractCollection; 11 | import java.util.Iterator; 12 | import java.util.Map; 13 | import java.util.Map.Entry; 14 | import java.util.Set; 15 | import java.util.function.BiFunction; 16 | import java.util.function.Function; 17 | 18 | @SuppressWarnings("rawtypes") 19 | public class FressianWriteHandler> implements WriteHandler { 20 | private final Class type; 21 | private final String tag; 22 | private final Set cachedKeys; 23 | 24 | public FressianWriteHandler(Class type, String tag, Set cachedKeys) { 25 | this.type = type; 26 | this.tag = tag; 27 | this.cachedKeys = cachedKeys; 28 | } 29 | 30 | @Override 31 | public void write(Writer w, Object instance) throws IOException { 32 | // We manually serialize the backing map so that we can apply caching transformations to specific subcomponents. 33 | // To avoid needless copying we do this via an adapter rather than copying to a temporary list. 34 | w.writeTag(tag, 1); 35 | w.writeTag("map", 1); 36 | 37 | Map map = ((DynamicObject) instance).getMap(); 38 | w.writeList(new TransformedMap(map, this::transformKey, this::transformValue)); 39 | } 40 | 41 | /* 42 | * Although Fressian will automatically cache the string components of each Keyword, by default we still spend a 43 | * minimum of three bytes per keyword - one for the keyword directive itself, one for the namespace (usually null), 44 | * and one for the cached reference to the keyword's string. By requesting caching of the Keyword object itself like 45 | * this, we can get this down to one byte (after the initial cache miss). 46 | * 47 | * The downside of this is that cache misses incur additional an additional byte marking the key as being LRU-cache 48 | * capable, and the cache misses will also result in two cache entries being introduced, causing more cache churn 49 | * than there would be otherwise. 50 | */ 51 | private Object transformKey(Object key) { 52 | return new CachedObject(key); 53 | } 54 | 55 | @SuppressWarnings("unchecked") 56 | private Object transformValue(Object key, Object value) { 57 | if (cachedKeys.contains(key)) { 58 | return new CachedObject(value); 59 | } 60 | 61 | return value; 62 | } 63 | 64 | @Immutable 65 | @SuppressWarnings("unchecked") 66 | private static class TransformedMap extends AbstractCollection { 67 | private final Map backingMap; 68 | private final Function keysTransformation; 69 | private final BiFunction valuesTransformation; 70 | 71 | private TransformedMap( 72 | Map backingMap, 73 | Function keysTransformation, 74 | BiFunction valuesTransformation 75 | ) { 76 | this.backingMap = backingMap; 77 | this.keysTransformation = keysTransformation; 78 | this.valuesTransformation = valuesTransformation; 79 | } 80 | 81 | @Override 82 | public Iterator iterator() { 83 | return new TransformingKeyValueIterator(backingMap.entrySet().iterator(), keysTransformation, valuesTransformation); 84 | } 85 | 86 | @Override 87 | public int size() { 88 | return backingMap.size() * 2; 89 | } 90 | } 91 | 92 | @NotThreadSafe 93 | private static class TransformingKeyValueIterator implements Iterator { 94 | private final Iterator entryIterator; 95 | private final Function keysTransformation; 96 | private final BiFunction valuesTransformation; 97 | 98 | Object pendingValue = null; 99 | boolean hasPendingValue = false; 100 | 101 | private TransformingKeyValueIterator( 102 | Iterator entryIterator, 103 | Function keysTransformation, 104 | BiFunction valuesTransformation 105 | ) { 106 | this.entryIterator = entryIterator; 107 | this.keysTransformation = keysTransformation; 108 | this.valuesTransformation = valuesTransformation; 109 | } 110 | 111 | @Override 112 | public boolean hasNext() { 113 | return hasPendingValue || entryIterator.hasNext(); 114 | } 115 | 116 | @Override 117 | public Object next() { 118 | if (hasPendingValue) { 119 | Object value = pendingValue; 120 | pendingValue = null; 121 | hasPendingValue = false; 122 | 123 | return value; 124 | } 125 | 126 | Map.Entry entry = entryIterator.next(); 127 | pendingValue = valuesTransformation.apply(entry.getKey(), entry.getValue()); 128 | 129 | hasPendingValue = true; 130 | 131 | return keysTransformation.apply(entry.getKey()); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/Key.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 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 | @Target({ElementType.METHOD}) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface Key { 11 | String value(); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/Meta.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 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 | * Mark a field as metadata. Metadata is used to annotate a value with additional information that is not logically 10 | * considered part of the value. Metadata is ignored for the purposes of equality and serialization. 11 | */ 12 | @Target({ElementType.METHOD}) 13 | @Retention(RetentionPolicy.RUNTIME) 14 | public @interface Meta { 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/Required.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 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 | * Marks a field as required. Fields marked with this annotation will throw a NullPointerException when accessed if a 10 | * nonnull value is not present, or when the validate() method is called on the instance. 11 | */ 12 | @Target({ElementType.METHOD}) 13 | @Retention(RetentionPolicy.RUNTIME) 14 | public @interface Required { 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/Unknown.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import java.io.IOException; 4 | import java.io.StringWriter; 5 | import java.io.Writer; 6 | import java.util.Map; 7 | 8 | import com.github.rschmitt.dynamicobject.internal.ClojureStuff; 9 | 10 | /** 11 | * A generic container for tagged Edn elements. This class preserves everything the Edn reader sees when an unknown 12 | * reader tag is encountered. 13 | */ 14 | public class Unknown { 15 | private final String tag; 16 | private final Object element; 17 | 18 | /** 19 | * For internal use only. Serialize a tagged element of an unknown type. 20 | */ 21 | @SuppressWarnings("unused") 22 | public static Object serialize(Unknown unknown, Writer w) throws IOException { 23 | w.append('#'); 24 | w.append(unknown.getTag()); 25 | if (!(unknown.getElement() instanceof Map)) 26 | w.append(' '); 27 | ClojureStuff.PrOn.invoke(unknown.getElement(), w); 28 | return null; 29 | } 30 | 31 | public Unknown(String tag, Object element) { 32 | this.tag = tag; 33 | this.element = element; 34 | } 35 | 36 | public String getTag() { 37 | return tag; 38 | } 39 | 40 | public Object getElement() { 41 | return element; 42 | } 43 | 44 | @Override 45 | public boolean equals(Object o) { 46 | if (this == o) return true; 47 | if (o == null || getClass() != o.getClass()) return false; 48 | 49 | Unknown unknown = (Unknown) o; 50 | 51 | if (!element.equals(unknown.element)) return false; 52 | if (!tag.equals(unknown.tag)) return false; 53 | 54 | return true; 55 | } 56 | 57 | @Override 58 | public int hashCode() { 59 | int result = tag.hashCode(); 60 | result = 31 * result + element.hashCode(); 61 | return result; 62 | } 63 | 64 | @Override 65 | public String toString() { 66 | try { 67 | StringWriter stringWriter = new StringWriter(); 68 | serialize(this, stringWriter); 69 | return stringWriter.toString(); 70 | } catch (IOException ex) { 71 | throw new RuntimeException(ex); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/internal/ClojureStuff.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject.internal; 2 | 3 | import clojure.lang.IFn; 4 | 5 | import java.util.Map; 6 | 7 | import static clojure.java.api.Clojure.read; 8 | import static clojure.java.api.Clojure.var; 9 | 10 | @SuppressWarnings("rawtypes") 11 | public class ClojureStuff { 12 | public static final Map EmptyMap = (Map) read("{}"); 13 | public static final Object EmptySet = read("#{}"); 14 | public static final Object EmptyVector = read("[]"); 15 | public static final Object Readers = read(":readers"); 16 | public static final Object Default = read(":default"); 17 | 18 | public static final IFn Assoc = var("clojure.core/assoc"); 19 | public static final IFn AssocBang = var("clojure.core/assoc!"); 20 | public static final IFn Bigint = var("clojure.core/bigint"); 21 | public static final IFn Biginteger = var("clojure.core/biginteger"); 22 | public static final IFn ConjBang = var("clojure.core/conj!"); 23 | public static final IFn Deref = var("clojure.core/deref"); 24 | public static final IFn Dissoc = var("clojure.core/dissoc"); 25 | public static final IFn Eval = var("clojure.core/eval"); 26 | public static final IFn Get = var("clojure.core/get"); 27 | public static final IFn Memoize = var("clojure.core/memoize"); 28 | public static final IFn MergeWith = var("clojure.core/merge-with"); 29 | public static final IFn Meta = var("clojure.core/meta"); 30 | public static final IFn Nth = var("clojure.core/nth"); 31 | public static final IFn Persistent = var("clojure.core/persistent!"); 32 | public static final IFn PreferMethod = var("clojure.core/prefer-method"); 33 | public static final IFn PrOn = var("clojure.core/pr-on"); 34 | public static final IFn Read = var("clojure.edn/read"); 35 | public static final IFn ReadString = var("clojure.edn/read-string"); 36 | public static final IFn RemoveMethod = var("clojure.core/remove-method"); 37 | public static final IFn Transient = var("clojure.core/transient"); 38 | public static final IFn VaryMeta = var("clojure.core/vary-meta"); 39 | 40 | public static final Object PrintMethod = Deref.invoke(var("clojure.core/print-method")); 41 | public static final IFn CachedRead = (IFn) Memoize.invoke(var("clojure.edn/read-string")); 42 | public static final IFn Pprint; 43 | public static final IFn SimpleDispatch; 44 | public static final IFn Diff; 45 | 46 | public static final Map clojureReadHandlers; 47 | public static final Map clojureWriteHandlers; 48 | 49 | static { 50 | IFn require = var("clojure.core/require"); 51 | require.invoke(read("clojure.pprint")); 52 | require.invoke(read("clojure.data")); 53 | require.invoke(read("clojure.data.fressian")); 54 | 55 | Pprint = var("clojure.pprint/pprint"); 56 | Diff = var("clojure.data/diff"); 57 | 58 | SimpleDispatch = (IFn) Deref.invoke(var("clojure.pprint/simple-dispatch")); 59 | 60 | clojureReadHandlers = (Map) Deref.invoke(var("clojure.data.fressian/clojure-read-handlers")); 61 | clojureWriteHandlers = (Map) Deref.invoke(var("clojure.data.fressian/clojure-write-handlers")); 62 | } 63 | 64 | public static Object cachedRead(String edn) { 65 | return CachedRead.invoke(edn); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/internal/Conversions.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject.internal; 2 | 3 | import com.github.rschmitt.collider.ClojureMap; 4 | import com.github.rschmitt.collider.Collider; 5 | import com.github.rschmitt.collider.TransientMap; 6 | import com.github.rschmitt.dynamicobject.DynamicObject; 7 | 8 | import java.lang.reflect.ParameterizedType; 9 | import java.lang.reflect.Type; 10 | import java.time.Instant; 11 | import java.util.Arrays; 12 | import java.util.Collection; 13 | import java.util.Date; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.Optional; 17 | import java.util.Set; 18 | 19 | @SuppressWarnings("rawtypes") 20 | class Conversions { 21 | /* 22 | * Convert a Java object (e.g. passed in to a builder method) into the Clojure-style representation used internally. 23 | * This is done according to the following rules: 24 | * * Boxed and unboxed numerics, as well as BigInteger, will be losslessly converted to Long, Double, or BigInt. 25 | * * Values wrapped in an Optional will be unwrapped and stored as either null or the underlying value. 26 | * * Supported collection types (List, Set, Map) will have their elements converted according to these rules. This 27 | * also applies to nested collections. For instance, a List> will effectively be converted to a 28 | * List>. 29 | */ 30 | static Object javaToClojure(Object obj) { 31 | Object val = Numerics.maybeUpconvert(obj); 32 | if (val instanceof DynamicObject) 33 | return obj; 34 | else if (val instanceof Instant) 35 | return java.util.Date.from((Instant) val); 36 | else if (val instanceof List) 37 | return convertCollectionToClojureTypes((Collection) val, ClojureStuff.EmptyVector); 38 | else if (val instanceof Set) 39 | return convertCollectionToClojureTypes((Collection) val, ClojureStuff.EmptySet); 40 | else if (val instanceof Map) 41 | return convertMapToClojureTypes((Map) val); 42 | else if (val instanceof Optional) { 43 | Optional opt = (Optional) val; 44 | if (opt.isPresent()) 45 | return javaToClojure(opt.get()); 46 | else 47 | return null; 48 | } else 49 | return val; 50 | } 51 | 52 | private static Object convertCollectionToClojureTypes(Collection val, Object empty) { 53 | Object ret = ClojureStuff.Transient.invoke(empty); 54 | for (Object o : val) 55 | ret = ClojureStuff.ConjBang.invoke(ret, javaToClojure(o)); 56 | return ClojureStuff.Persistent.invoke(ret); 57 | } 58 | 59 | private static Object convertMapToClojureTypes(Map map) { 60 | Object ret = ClojureStuff.Transient.invoke(ClojureStuff.EmptyMap); 61 | for (Map.Entry entry : map.entrySet()) 62 | ret = ClojureStuff.AssocBang.invoke(ret, javaToClojure(entry.getKey()), javaToClojure(entry.getValue())); 63 | return ClojureStuff.Persistent.invoke(ret); 64 | } 65 | 66 | /* 67 | * Convert a Clojure object (i.e. a value somewhere in a DynamicObject's map) into the expected Java representation. 68 | * This representation is determined by the generic return type of the method. The conversion is performed as 69 | * follows: 70 | * * If the return type is a numeric type, the Clojure numeric will be downconverted to the expected type (e.g. 71 | * Long -> Integer). 72 | * * If the return type is a nested DynamicObject, we wrap the Clojure value as the expected DynamicObject type. 73 | * * If the return type is an Optional, we convert the value and then wrap it by calling Optional#ofNullable. 74 | * * If the return type is a collection type, there are a few possibilities: 75 | * * If it is a raw type, no action is taken. 76 | * * If it is a wildcard type (e.g. List), an UnsupportedOperationException is thrown. 77 | * * If the type variable is a Class, the elements of the collection are enumerated over to convert numerics and 78 | * wraps DynamicObjects. 79 | * * If the type variable is another collection type, the algorithm recurses. 80 | */ 81 | @SuppressWarnings("unchecked") 82 | static Object clojureToJava(Object obj, Type genericReturnType) { 83 | Class rawReturnType = Reflection.getRawType(genericReturnType); 84 | if (rawReturnType.equals(Optional.class)) { 85 | Type nestedType = Reflection.getTypeArgument(genericReturnType, 0); 86 | return Optional.ofNullable(clojureToJava(obj, nestedType)); 87 | } 88 | 89 | if (obj == null) return null; 90 | if (genericReturnType instanceof Class) { 91 | Class returnType = (Class) genericReturnType; 92 | if (Numerics.isNumeric(returnType)) 93 | return Numerics.maybeDownconvert(returnType, obj); 94 | if (Instant.class.equals(returnType)) 95 | return ((Date) obj).toInstant(); 96 | if (DynamicObject.class.isAssignableFrom(returnType)) 97 | return DynamicObject.wrap((Map) obj, (Class) returnType); 98 | } 99 | 100 | if (obj instanceof List) { 101 | obj = ((Collection)obj).stream().map( 102 | elem -> convertCollectionElementToJavaTypes(elem, genericReturnType) 103 | ).collect(Collider.toClojureList()); 104 | } else if (obj instanceof Set) { 105 | obj = ((Collection)obj).stream().map( 106 | elem -> convertCollectionElementToJavaTypes(elem, genericReturnType) 107 | ).collect(Collider.toClojureSet()); 108 | } else if (obj instanceof Map) { 109 | obj = convertMapToJavaTypes((Map) obj, genericReturnType); 110 | if (rawReturnType.equals(ClojureMap.class)) 111 | return Collider.intoClojureMap((Map) obj); 112 | } 113 | return obj; 114 | } 115 | 116 | private static Object convertCollectionElementToJavaTypes(Object element, Type genericCollectionType) { 117 | if (genericCollectionType instanceof ParameterizedType) { 118 | ParameterizedType parameterizedType = (ParameterizedType) genericCollectionType; 119 | List typeArgs = Arrays.asList(parameterizedType.getActualTypeArguments()); 120 | assert typeArgs.size() == 1; 121 | return clojureToJava(element, typeArgs.get(0)); 122 | } else 123 | return clojureToJava(element, Object.class); 124 | } 125 | 126 | private static Object convertMapToJavaTypes(Map unwrappedMap, Type genericReturnType) { 127 | Type keyType, valType; 128 | if (genericReturnType instanceof ParameterizedType) { 129 | Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments(); 130 | assert actualTypeArguments.length == 2; 131 | keyType = actualTypeArguments[0]; 132 | valType = actualTypeArguments[1]; 133 | } else { 134 | keyType = valType = Object.class; 135 | } 136 | 137 | TransientMap transientMap = Collider.transientMap(); 138 | 139 | for (Map.Entry entry : unwrappedMap.entrySet()) 140 | transientMap.put(clojureToJava(entry.getKey(), keyType), clojureToJava(entry.getValue(), valType)); 141 | 142 | return transientMap.toPersistent(); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/internal/CustomValidationHook.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject.internal; 2 | 3 | import com.github.rschmitt.dynamicobject.DynamicObject; 4 | 5 | public interface CustomValidationHook> { 6 | D $$customValidate(); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/internal/DynamicObjectInstance.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject.internal; 2 | 3 | import clojure.lang.*; 4 | import com.github.rschmitt.dynamicobject.DynamicObject; 5 | 6 | import java.io.StringWriter; 7 | import java.io.Writer; 8 | import java.lang.reflect.Type; 9 | import java.util.Collection; 10 | import java.util.Iterator; 11 | import java.util.Map; 12 | import java.util.Set; 13 | import java.util.concurrent.ConcurrentHashMap; 14 | 15 | import static java.lang.String.format; 16 | 17 | @SuppressWarnings({"rawtypes", "unchecked"}) 18 | public abstract class DynamicObjectInstance> extends AFn implements Map, IPersistentMap, IObj, MapEquivalence, IHashEq, DynamicObjectPrintHook, CustomValidationHook { 19 | private static final Object Default = new Object(); 20 | private static final Object Null = new Object(); 21 | 22 | private final Map map; 23 | private final Class type; 24 | private final ConcurrentHashMap valueCache = new ConcurrentHashMap(); 25 | 26 | public DynamicObjectInstance(Map map, Class type) { 27 | this.map = map; 28 | this.type = type; 29 | } 30 | 31 | public Map getMap() { 32 | return map; 33 | } 34 | 35 | public Class getType() { 36 | return type; 37 | } 38 | 39 | @Override 40 | public String toString() { 41 | return DynamicObject.serialize(this); 42 | } 43 | 44 | @Override 45 | public int hashCode() { 46 | return map.hashCode(); 47 | } 48 | 49 | @Override 50 | public boolean equals(Object other) { 51 | if (other == this) return true; 52 | if (other == null) return false; 53 | 54 | if (other instanceof DynamicObject) 55 | return map.equals(((DynamicObject) other).getMap()); 56 | else 57 | return other.equals(map); 58 | } 59 | 60 | public void prettyPrint() { 61 | ClojureStuff.Pprint.invoke(this); 62 | } 63 | 64 | public String toFormattedString() { 65 | Writer w = new StringWriter(); 66 | ClojureStuff.Pprint.invoke(this, w); 67 | return w.toString(); 68 | } 69 | 70 | public D merge(D other) { 71 | AFn ignoreNulls = new AFn() { 72 | public Object invoke(Object arg1, Object arg2) { 73 | return (arg2 == null) ? arg1 : arg2; 74 | } 75 | }; 76 | Map mergedMap = (Map) ClojureStuff.MergeWith.invoke(ignoreNulls, map, other.getMap()); 77 | return DynamicObject.wrap(mergedMap, type); 78 | } 79 | 80 | public D intersect(D arg) { 81 | return diff(arg, 2); 82 | } 83 | 84 | public D subtract(D arg) { 85 | return diff(arg, 0); 86 | } 87 | 88 | private D diff(D arg, int idx) { 89 | Object array = ClojureStuff.Diff.invoke(map, arg.getMap()); 90 | Object union = ClojureStuff.Nth.invoke(array, idx); 91 | if (union == null) union = ClojureStuff.EmptyMap; 92 | return DynamicObject.wrap((Map) union, type); 93 | } 94 | 95 | public D convertAndAssoc(Object key, Object value) { 96 | return (D) assoc(key, Conversions.javaToClojure(value)); 97 | } 98 | 99 | @Override 100 | public IPersistentMap assoc(Object key, Object value) { 101 | return (DynamicObjectInstance) DynamicObject.wrap((Map) ClojureStuff.Assoc.invoke(map, key, value), type); 102 | } 103 | 104 | public D assocMeta(Object key, Object value) { 105 | return DynamicObject.wrap((Map) ClojureStuff.VaryMeta.invoke(map, ClojureStuff.Assoc, key, value), type); 106 | } 107 | 108 | public Object getMetadataFor(Object key) { 109 | Object meta = ClojureStuff.Meta.invoke(map); 110 | return ClojureStuff.Get.invoke(meta, key); 111 | } 112 | 113 | public Object invokeGetter(Object key, boolean isRequired, Type genericReturnType) { 114 | Object value = getAndCacheValueFor(key, genericReturnType); 115 | if (value == null && isRequired) 116 | throw new NullPointerException(format("Required field %s was null", key.toString())); 117 | return value; 118 | } 119 | 120 | @SuppressWarnings("unchecked") 121 | public Object getAndCacheValueFor(Object key, Type genericReturnType) { 122 | Object cachedValue = valueCache.getOrDefault(key, Default); 123 | if (cachedValue == Null) return null; 124 | if (cachedValue != Default) return cachedValue; 125 | Object value = getValueFor(key, genericReturnType); 126 | if (value == null) 127 | valueCache.putIfAbsent(key, Null); 128 | else 129 | valueCache.putIfAbsent(key, value); 130 | return value; 131 | } 132 | 133 | public Object getValueFor(Object key, Type genericReturnType) { 134 | Object val = map.get(key); 135 | return Conversions.clojureToJava(val, genericReturnType); 136 | } 137 | 138 | public Object $$noop() { 139 | return this; 140 | } 141 | 142 | @Override 143 | public int size() { 144 | return map.size(); 145 | } 146 | 147 | @Override 148 | public boolean isEmpty() { 149 | return map.isEmpty(); 150 | } 151 | 152 | @Override 153 | public boolean containsKey(Object key) { 154 | return map.containsKey(key); 155 | } 156 | 157 | @Override 158 | public boolean containsValue(Object value) { 159 | return map.containsValue(value); 160 | } 161 | 162 | @Override 163 | public Object get(Object key) { 164 | return map.get(key); 165 | } 166 | 167 | @Override 168 | public Object put(Object key, Object value) { 169 | throw new UnsupportedOperationException(); 170 | } 171 | 172 | @Override 173 | public Object remove(Object key) { 174 | throw new UnsupportedOperationException(); 175 | } 176 | 177 | @Override 178 | public void putAll(Map m) { 179 | throw new UnsupportedOperationException(); 180 | } 181 | 182 | @Override 183 | public void clear() { 184 | throw new UnsupportedOperationException(); 185 | } 186 | 187 | @Override 188 | public Set keySet() { 189 | return map.keySet(); 190 | } 191 | 192 | @Override 193 | public Collection values() { 194 | return map.values(); 195 | } 196 | 197 | @Override 198 | public Set entrySet() { 199 | return map.entrySet(); 200 | } 201 | 202 | @Override 203 | public IMapEntry entryAt(Object key) { 204 | return ((Associative) map).entryAt(key); 205 | } 206 | 207 | @Override 208 | public Object valAt(Object key) { 209 | return ((Associative) map).valAt(key); 210 | } 211 | 212 | @Override 213 | public Object valAt(Object key, Object notFound) { 214 | return ((Associative) map).valAt(key, notFound); 215 | } 216 | 217 | @Override 218 | public int count() { 219 | return ((IPersistentCollection) map).count(); 220 | } 221 | 222 | @Override 223 | public IPersistentCollection cons(Object o) { 224 | Map newMap = (Map) ((IPersistentCollection) map).cons(o); 225 | return (DynamicObjectInstance) DynamicObject.wrap(newMap, type); 226 | } 227 | 228 | @Override 229 | public IPersistentCollection empty() { 230 | return (DynamicObjectInstance) DynamicObject.wrap(ClojureStuff.EmptyMap, type); 231 | } 232 | 233 | @Override 234 | public boolean equiv(Object o) { 235 | return ((IPersistentCollection) map).equiv(o); 236 | } 237 | 238 | @Override 239 | public ISeq seq() { 240 | return ((Seqable) map).seq(); 241 | } 242 | 243 | @Override 244 | public IPersistentMap assocEx(Object key, Object val) { 245 | Object newMap = ((IPersistentMap) map).assocEx(key, val); 246 | return (DynamicObjectInstance) DynamicObject.wrap((Map) newMap, type); 247 | } 248 | 249 | @Override 250 | public IPersistentMap without(Object key) { 251 | Object newMap = ((IPersistentMap) map).without(key); 252 | return (DynamicObjectInstance) DynamicObject.wrap((Map) newMap, type); 253 | } 254 | 255 | @Override 256 | public IPersistentMap meta() { 257 | return (IPersistentMap) ClojureStuff.Meta.invoke(map); 258 | } 259 | 260 | @Override 261 | public IObj withMeta(IPersistentMap meta) { 262 | Object newMap = ClojureStuff.VaryMeta.invoke(map, meta); 263 | return (DynamicObjectInstance) DynamicObject.wrap((Map) newMap, type); 264 | } 265 | 266 | @Override 267 | public int hasheq() { 268 | return ((IHashEq) map).hasheq(); 269 | } 270 | 271 | @Override 272 | public Object invoke(Object arg1) { 273 | return valAt(arg1); 274 | } 275 | 276 | @Override 277 | public Object invoke(Object arg1, Object notFound) { 278 | return valAt(arg1, notFound); 279 | } 280 | 281 | @Override 282 | public Iterator iterator() { 283 | return ((IPersistentMap) map).iterator(); 284 | } 285 | 286 | public Iterator valIterator() throws ReflectiveOperationException { 287 | Class aClass = map.getClass(); 288 | return (Iterator) aClass.getMethod("valIterator").invoke(map); 289 | } 290 | 291 | public Iterator keyIterator() throws ReflectiveOperationException { 292 | Class aClass = map.getClass(); 293 | return (Iterator) aClass.getMethod("keyIterator").invoke(map); 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/internal/DynamicObjectPrintHook.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject.internal; 2 | 3 | // Marker interface for print-method dispatch 4 | public interface DynamicObjectPrintHook { 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/internal/EdnSerialization.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject.internal; 2 | 3 | import clojure.java.api.Clojure; 4 | import clojure.lang.AFn; 5 | import clojure.lang.IPersistentMap; 6 | import com.github.rschmitt.dynamicobject.DynamicObject; 7 | import com.github.rschmitt.dynamicobject.EdnTranslator; 8 | import com.github.rschmitt.dynamicobject.Unknown; 9 | 10 | import java.io.IOException; 11 | import java.io.PushbackReader; 12 | import java.io.StringReader; 13 | import java.io.StringWriter; 14 | import java.io.Writer; 15 | import java.util.Iterator; 16 | import java.util.Map; 17 | import java.util.NoSuchElementException; 18 | import java.util.Spliterator; 19 | import java.util.Spliterators; 20 | import java.util.concurrent.ConcurrentHashMap; 21 | import java.util.concurrent.atomic.AtomicReference; 22 | import java.util.function.BiFunction; 23 | import java.util.stream.Stream; 24 | import java.util.stream.StreamSupport; 25 | 26 | import static com.github.rschmitt.dynamicobject.internal.ClojureStuff.*; 27 | import static java.lang.String.format; 28 | 29 | @SuppressWarnings({"rawtypes", "unchecked"}) 30 | public class EdnSerialization { 31 | static { 32 | String clojureCode = 33 | "(defmethod print-method com.github.rschmitt.dynamicobject.internal.DynamicObjectPrintHook " + 34 | "[o, ^java.io.Writer w]" + 35 | "(com.github.rschmitt.dynamicobject.internal.EdnSerialization/invokePrintMethod o w))"; 36 | Eval.invoke(ReadString.invoke(clojureCode)); 37 | PreferMethod.invoke(PrintMethod, DynamicObjectPrintHook.class, IPersistentMap.class); 38 | PreferMethod.invoke(PrintMethod, DynamicObjectPrintHook.class, Map.class); 39 | 40 | clojureCode = 41 | "(defmethod clojure.pprint/simple-dispatch com.github.rschmitt.dynamicobject.internal.DynamicObjectPrintHook " + 42 | "[o] " + 43 | "(com.github.rschmitt.dynamicobject.internal.EdnSerialization/invokePrettyPrint o))"; 44 | Eval.invoke(ReadString.invoke(clojureCode)); 45 | PreferMethod.invoke(SimpleDispatch, DynamicObjectPrintHook.class, IPersistentMap.class); 46 | PreferMethod.invoke(SimpleDispatch, DynamicObjectPrintHook.class, Map.class); 47 | } 48 | 49 | public static class DynamicObjectPrintMethod extends AFn { 50 | @Override 51 | public Object invoke(Object arg1, Object arg2) { 52 | DynamicObject dynamicObject = (DynamicObject) arg1; 53 | Writer writer = (Writer) arg2; 54 | String tag = recordTagCache.getOrDefault(dynamicObject.getType(), null); 55 | try { 56 | if (tag != null) { 57 | writer.write("#"); 58 | writer.write(tag); 59 | } 60 | ClojureStuff.PrOn.invoke(dynamicObject.getMap(), writer); 61 | } catch (IOException ex) { 62 | throw new RuntimeException(ex); 63 | } 64 | return null; 65 | } 66 | } 67 | 68 | public static class DynamicObjectPrettyPrint extends AFn { 69 | @Override 70 | public Object invoke(Object arg1) { 71 | Object arg2 = Deref.invoke(Clojure.var("clojure.core/*out*")); 72 | DynamicObject dynamicObject = (DynamicObject) arg1; 73 | Writer writer = (Writer) arg2; 74 | String tag = recordTagCache.getOrDefault(dynamicObject.getType(), null); 75 | try { 76 | if (tag != null) { 77 | writer.write("#"); 78 | writer.write(tag); 79 | } 80 | SimpleDispatch.invoke(dynamicObject.getMap()); 81 | } catch (IOException ex) { 82 | throw new RuntimeException(ex); 83 | } 84 | return null; 85 | } 86 | } 87 | 88 | private static final DynamicObjectPrettyPrint dynamicObjectPrettyPrint = new DynamicObjectPrettyPrint(); 89 | private static final DynamicObjectPrintMethod dynamicObjectPrintMethod = new DynamicObjectPrintMethod(); 90 | private static final AtomicReference translators = new AtomicReference<>(ClojureStuff.EmptyMap); 91 | private static final ConcurrentHashMap, EdnTranslatorAdapter> translatorCache = new ConcurrentHashMap<>(); 92 | private static final AtomicReference defaultReader = new AtomicReference<>(getUnknownReader()); 93 | private static final ConcurrentHashMap, String> recordTagCache = new ConcurrentHashMap<>(); 94 | private static final Object EOF = Clojure.read(":eof"); 95 | 96 | public static String serialize(Object obj) { 97 | StringWriter stringWriter = new StringWriter(); 98 | serialize(obj, stringWriter); 99 | return stringWriter.toString(); 100 | } 101 | 102 | public static void serialize(Object object, Writer writer) { 103 | ClojureStuff.PrOn.invoke(object, writer); 104 | try { 105 | writer.flush(); 106 | } catch (IOException ex) { 107 | throw new RuntimeException(ex); 108 | } 109 | } 110 | 111 | public static T deserialize(String edn, Class type) { 112 | return deserialize(new PushbackReader(new StringReader(edn)), type); 113 | } 114 | 115 | @SuppressWarnings({"unchecked", "deprecation"}) 116 | static > T deserialize(PushbackReader streamReader, Class type) { 117 | Object opts = getReadOptions(); 118 | opts = ClojureStuff.Assoc.invoke(opts, EOF, EOF); 119 | Object obj = ClojureStuff.Read.invoke(opts, streamReader); 120 | if (EOF.equals(obj)) 121 | throw new NoSuchElementException(); 122 | if (DynamicObject.class.isAssignableFrom(type) && !(obj instanceof DynamicObject)) { 123 | obj = Instances.wrap((Map) obj, (Class) type).afterDeserialization(); 124 | } 125 | return type.cast(obj); 126 | } 127 | 128 | public static Stream deserializeStream(PushbackReader streamReader, Class type) { 129 | Iterator iterator = Serialization.deserializeStreamToIterator(() -> deserialize(streamReader, type), type); 130 | Spliterator spliterator = Spliterators.spliteratorUnknownSize(iterator, Spliterator.IMMUTABLE); 131 | return StreamSupport.stream(spliterator, false); 132 | } 133 | 134 | private static AFn getUnknownReader() { 135 | String clojureCode = format("(defmethod print-method %s [o, ^java.io.Writer w]" + 136 | "(com.github.rschmitt.dynamicobject.Unknown/serialize o w))", Unknown.class.getTypeName()); 137 | ClojureStuff.Eval.invoke(ClojureStuff.ReadString.invoke(clojureCode)); 138 | return wrapReaderFunction(Unknown::new); 139 | } 140 | 141 | public static void setDefaultReader(BiFunction reader) { 142 | if (reader == null) { 143 | defaultReader.set(null); 144 | return; 145 | } 146 | defaultReader.set(wrapReaderFunction(reader)); 147 | } 148 | 149 | private static AFn wrapReaderFunction(BiFunction reader) { 150 | return new AFn() { 151 | @Override 152 | public Object invoke(Object arg1, Object arg2) { 153 | return reader.apply(arg1.toString(), arg2); 154 | } 155 | }; 156 | } 157 | 158 | private static Object getReadOptions() { 159 | Object map = ClojureStuff.Assoc.invoke(ClojureStuff.EmptyMap, ClojureStuff.Readers, translators.get()); 160 | AFn defaultReader = EdnSerialization.defaultReader.get(); 161 | if (defaultReader != null) { 162 | map = ClojureStuff.Assoc.invoke(map, ClojureStuff.Default, defaultReader); 163 | } 164 | return map; 165 | } 166 | 167 | public static synchronized void registerType(Class type, EdnTranslator translator) { 168 | EdnTranslatorAdapter adapter = new EdnTranslatorAdapter<>(translator); 169 | EdnTranslatorAdapter currentAdapter = translatorCache.put(type, adapter); 170 | if (currentAdapter != null) { 171 | // already registered 172 | return; 173 | } 174 | translators.getAndUpdate(translators -> ClojureStuff.Assoc.invoke(translators, ClojureStuff.cachedRead( 175 | translator.getTag()), adapter)); 176 | String clojureCode = format( 177 | "(defmethod print-method %s " + 178 | "[o, ^java.io.Writer w] " + 179 | "(com.github.rschmitt.dynamicobject.internal.EdnSerialization/invokeWriter o w \"%s\"))", 180 | type.getTypeName(), translator.getTag()); 181 | ClojureStuff.Eval.invoke(ClojureStuff.ReadString.invoke(clojureCode)); 182 | } 183 | 184 | public static synchronized void deregisterType(Class type) { 185 | EdnTranslatorAdapter adapter = (EdnTranslatorAdapter) translatorCache.get(type); 186 | translators.getAndUpdate(translators -> ClojureStuff.Dissoc.invoke(translators, ClojureStuff.cachedRead( 187 | adapter.getTag()))); 188 | ClojureStuff.RemoveMethod.invoke(PrintMethod, adapter); 189 | translatorCache.remove(type); 190 | } 191 | 192 | public static synchronized > void registerTag(Class type, String tag) { 193 | String currentTagForType = recordTagCache.put(type, tag); 194 | if (currentTagForType != null) { 195 | // already registered 196 | return; 197 | } 198 | 199 | translators.getAndUpdate(translators -> ClojureStuff.Assoc.invoke(translators, ClojureStuff.cachedRead( 200 | tag), new RecordReader<>(type))); 201 | } 202 | 203 | public static synchronized > void deregisterTag(Class type) { 204 | String tag = recordTagCache.get(type); 205 | translators.getAndUpdate(translators -> ClojureStuff.Dissoc.invoke(translators, ClojureStuff.cachedRead(tag))); 206 | recordTagCache.remove(type); 207 | } 208 | 209 | @SuppressWarnings("unused") 210 | public static Object invokeWriter(Object obj, Writer writer, String tag) { 211 | EdnTranslatorAdapter translator = (EdnTranslatorAdapter) ClojureStuff.Get.invoke(translators.get(), ClojureStuff 212 | .cachedRead(tag)); 213 | return translator.invoke(obj, writer); 214 | } 215 | 216 | public static Object invokePrintMethod(Object arg1, Object arg2) { 217 | return dynamicObjectPrintMethod.invoke(arg1, arg2); 218 | } 219 | 220 | public static Object invokePrettyPrint(Object o) { 221 | return dynamicObjectPrettyPrint.invoke(o); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/internal/EdnTranslatorAdapter.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject.internal; 2 | 3 | import java.io.IOException; 4 | import java.io.Writer; 5 | 6 | import com.github.rschmitt.dynamicobject.EdnTranslator; 7 | import clojure.lang.AFn; 8 | 9 | final class EdnTranslatorAdapter extends AFn { 10 | private final EdnTranslator ednTranslator; 11 | 12 | EdnTranslatorAdapter(EdnTranslator ednTranslator) { 13 | this.ednTranslator = ednTranslator; 14 | } 15 | 16 | @Override 17 | public Object invoke(Object arg) { 18 | return ednTranslator.read(arg); 19 | } 20 | 21 | @Override 22 | @SuppressWarnings("unchecked") 23 | public Object invoke(Object arg1, Object arg2) { 24 | Writer writer = (Writer) arg2; 25 | try { 26 | writer.write(String.format("#%s", ednTranslator.getTag())); 27 | ednTranslator.write((T) arg1, writer); 28 | } catch (IOException ex) { 29 | throw new RuntimeException(ex); 30 | } 31 | return null; 32 | } 33 | 34 | public final String getTag() { 35 | return ednTranslator.getTag(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/internal/FressianSerialization.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject.internal; 2 | 3 | import com.github.rschmitt.dynamicobject.DynamicObject; 4 | import com.github.rschmitt.dynamicobject.FressianReadHandler; 5 | import com.github.rschmitt.dynamicobject.FressianWriteHandler; 6 | import org.fressian.FressianReader; 7 | import org.fressian.FressianWriter; 8 | import org.fressian.handlers.ReadHandler; 9 | import org.fressian.handlers.WriteHandler; 10 | import org.fressian.impl.Handlers; 11 | import org.fressian.impl.InheritanceLookup; 12 | import org.fressian.impl.MapLookup; 13 | 14 | import java.io.ByteArrayInputStream; 15 | import java.io.ByteArrayOutputStream; 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.io.OutputStream; 19 | import java.util.Iterator; 20 | import java.util.Map; 21 | import java.util.Spliterator; 22 | import java.util.Spliterators; 23 | import java.util.concurrent.ConcurrentHashMap; 24 | import java.util.stream.Stream; 25 | import java.util.stream.StreamSupport; 26 | 27 | @SuppressWarnings({"rawtypes", "unchecked"}) 28 | public class FressianSerialization { 29 | private static final ConcurrentHashMap> fressianWriteHandlers = new ConcurrentHashMap<>(); 30 | private static final ConcurrentHashMap fressianReadHandlers = new ConcurrentHashMap<>(); 31 | private static final ConcurrentHashMap, String> binaryTagCache = new ConcurrentHashMap<>(); 32 | private static final ConcurrentHashMap, String> binaryTypeCache = new ConcurrentHashMap<>(); 33 | 34 | static { 35 | fressianWriteHandlers.putAll(ClojureStuff.clojureWriteHandlers); 36 | fressianReadHandlers.putAll(ClojureStuff.clojureReadHandlers); 37 | } 38 | 39 | public static Stream deserializeFressianStream(InputStream is, Class type) { 40 | FressianReader fressianReader = new FressianReader(is, new MapLookup<>(fressianReadHandlers)); 41 | Iterator iterator = Serialization.deserializeStreamToIterator(() -> (T) fressianReader.readObject(), type); 42 | Spliterator spliterator = Spliterators.spliteratorUnknownSize(iterator, Spliterator.IMMUTABLE); 43 | return StreamSupport.stream(spliterator, false); 44 | } 45 | 46 | public static FressianReader createFressianReader(InputStream is, boolean validateChecksum) { 47 | return new FressianReader(is, new MapLookup<>(fressianReadHandlers), validateChecksum); 48 | } 49 | 50 | public static FressianWriter createFressianWriter(OutputStream os) { 51 | return new FressianWriter(os, new InheritanceLookup<>(new MapLookup<>(fressianWriteHandlers))); 52 | } 53 | 54 | public static byte[] toFressianByteArray(Object o) { 55 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 56 | try (FressianWriter fressianWriter = DynamicObject.createFressianWriter(baos)) { 57 | fressianWriter.writeObject(o); 58 | } catch (IOException ex) { 59 | throw new RuntimeException(ex); 60 | } 61 | return baos.toByteArray(); 62 | } 63 | 64 | public static T fromFressianByteArray(byte[] bytes) { 65 | ByteArrayInputStream bais = new ByteArrayInputStream(bytes); 66 | FressianReader fressianReader = DynamicObject.createFressianReader(bais, false); 67 | try { 68 | return (T) fressianReader.readObject(); 69 | } catch (IOException ex) { 70 | throw new RuntimeException(ex); 71 | } 72 | } 73 | 74 | public static synchronized void registerType(Class type, String tag, ReadHandler readHandler, WriteHandler writeHandler) { 75 | String currentTagForType = binaryTypeCache.put(type, tag); 76 | if (currentTagForType != null) { 77 | // already registered 78 | return; 79 | } 80 | Handlers.installHandler(fressianWriteHandlers, type, tag, writeHandler); 81 | fressianReadHandlers.putIfAbsent(tag, readHandler); 82 | } 83 | 84 | static synchronized void deregisterType(Class type) { 85 | fressianWriteHandlers.remove(type); 86 | String tag = binaryTypeCache.remove(type); 87 | if (tag != null) { 88 | fressianReadHandlers.remove(tag); 89 | } 90 | } 91 | 92 | static synchronized > void registerTag(Class type, String tag) { 93 | String currentTagForType = binaryTagCache.put(type, tag); 94 | if (currentTagForType != null) { 95 | // already registered 96 | return; 97 | } 98 | Handlers.installHandler(fressianWriteHandlers, type, tag, new FressianWriteHandler(type, tag, Reflection.cachedKeys(type))); 99 | fressianReadHandlers.putIfAbsent(tag, new FressianReadHandler(type)); 100 | } 101 | 102 | static synchronized > void deregisterTag(Class type) { 103 | String tag = binaryTagCache.remove(type); 104 | fressianWriteHandlers.remove(type); 105 | if (tag != null) { 106 | fressianReadHandlers.remove(tag); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/internal/Instances.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject.internal; 2 | 3 | import com.github.rschmitt.dynamicobject.DynamicObject; 4 | import com.github.rschmitt.dynamicobject.internal.indyproxy.DynamicProxy; 5 | 6 | import java.util.Map; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | import java.util.concurrent.ConcurrentMap; 9 | 10 | import static com.github.rschmitt.dynamicobject.internal.ClojureStuff.EmptyMap; 11 | 12 | @SuppressWarnings("rawtypes") 13 | public class Instances { 14 | private static final ConcurrentMap proxyCache = new ConcurrentHashMap<>(); 15 | 16 | public static > D newInstance(Class type) { 17 | return wrap(EmptyMap, type); 18 | } 19 | 20 | public static > D wrap(Map map, Class type) { 21 | if (map == null) 22 | throw new NullPointerException("A null reference cannot be used as a DynamicObject"); 23 | if (map instanceof DynamicObject) 24 | return type.cast(map); 25 | 26 | return createIndyProxy(map, type); 27 | } 28 | 29 | private static > D createIndyProxy(Map map, Class type) { 30 | ensureInitialized(type); 31 | try { 32 | DynamicProxy dynamicProxy; 33 | // Use ConcurrentHashMap#computeIfAbsent only when key is not present to avoid locking: JDK-8161372 34 | if (proxyCache.containsKey(type)) { 35 | dynamicProxy = proxyCache.get(type); 36 | } else { 37 | dynamicProxy = proxyCache.computeIfAbsent(type, Instances::createProxy); 38 | } 39 | Object proxy = dynamicProxy 40 | .constructor() 41 | .invoke(map, type); 42 | return type.cast(proxy); 43 | } catch (Throwable t) { 44 | throw new RuntimeException(t); 45 | } 46 | } 47 | 48 | // This is to avoid hitting JDK-8062841 in the case where 'type' has a static field of type D 49 | // that has not yet been initialized. 50 | private static > void ensureInitialized(Class c) { 51 | if (!proxyCache.containsKey(c)) 52 | load(c); 53 | } 54 | 55 | private static void load(Class c) { 56 | try { 57 | Class.forName(c.getName()); 58 | } catch (ClassNotFoundException e) { 59 | throw new RuntimeException(e); 60 | } 61 | } 62 | 63 | private static DynamicProxy createProxy(Class dynamicObjectType) { 64 | String[] slices = dynamicObjectType.getName().split("\\."); 65 | String name = slices[slices.length - 1] + "Impl"; 66 | try { 67 | DynamicProxy.Builder builder = DynamicProxy.builder() 68 | .withInterfaces(dynamicObjectType, CustomValidationHook.class) 69 | .withSuperclass(DynamicObjectInstance.class) 70 | .withInvocationHandler(new InvokeDynamicInvocationHandler(dynamicObjectType)) 71 | .withConstructor(Map.class, Class.class) 72 | .withPackageName(dynamicObjectType.getPackage().getName()) 73 | .withClassName(name); 74 | try { 75 | Class iMapIterable = Class.forName("clojure.lang.IMapIterable"); 76 | builder = builder.withInterfaces(iMapIterable); 77 | } catch (ClassNotFoundException ignore) {} 78 | return builder.build(); 79 | } catch (Exception e) { 80 | throw new RuntimeException(e); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/internal/InvokeDynamicInvocationHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject.internal; 2 | 3 | import com.github.rschmitt.dynamicobject.DynamicObject; 4 | import com.github.rschmitt.dynamicobject.internal.indyproxy.DynamicInvocationHandler; 5 | 6 | import java.lang.invoke.CallSite; 7 | import java.lang.invoke.ConstantCallSite; 8 | import java.lang.invoke.MethodHandle; 9 | import java.lang.invoke.MethodHandles; 10 | import java.lang.invoke.MethodType; 11 | import java.lang.reflect.Method; 12 | import java.lang.reflect.Type; 13 | 14 | import static java.lang.invoke.MethodType.methodType; 15 | 16 | @SuppressWarnings("rawtypes") 17 | public class InvokeDynamicInvocationHandler implements DynamicInvocationHandler { 18 | private final Class dynamicObjectType; 19 | 20 | public InvokeDynamicInvocationHandler(Class dynamicObjectType) { 21 | this.dynamicObjectType = dynamicObjectType; 22 | } 23 | 24 | @Override 25 | @SuppressWarnings("unchecked") 26 | public CallSite handleInvocation( 27 | MethodHandles.Lookup lookup, 28 | String methodName, 29 | MethodType methodType, 30 | MethodHandle superMethod 31 | ) throws Throwable { 32 | Class proxyType = methodType.parameterArray()[0]; 33 | MethodHandle mh; 34 | if (superMethod != null && !"validate".equals(methodName)) { 35 | mh = superMethod.asType(methodType); 36 | return new ConstantCallSite(mh); 37 | } 38 | if ("validate".equals(methodName)) { 39 | mh = Validation.buildValidatorFor(dynamicObjectType).asType(methodType); 40 | } else if ("$$customValidate".equals(methodName)) { 41 | try { 42 | mh = lookup.findSpecial(dynamicObjectType, "validate", methodType(dynamicObjectType), proxyType); 43 | } catch (NoSuchMethodException ex) { 44 | mh = lookup.findSpecial(DynamicObjectInstance.class, "$$noop", methodType(Object.class, new Class[]{}), proxyType); 45 | } 46 | mh = mh.asType(methodType); 47 | } else if ("afterDeserialization".equals(methodName)) { 48 | mh = lookup.findSpecial(DynamicObjectInstance.class, "$$noop", methodType(Object.class, new Class[]{}), proxyType).asType(methodType); 49 | } else { 50 | Method method = dynamicObjectType.getMethod(methodName, methodType.dropParameterTypes(0, 1).parameterArray()); 51 | 52 | if (isBuilderMethod(method)) { 53 | Object key = Reflection.getKeyForBuilder(method); 54 | if (Reflection.isMetadataBuilder(method)) { 55 | mh = lookup.findSpecial(DynamicObjectInstance.class, "assocMeta", methodType(DynamicObject.class, Object.class, Object.class), proxyType); 56 | mh = MethodHandles.insertArguments(mh, 1, key); 57 | mh = mh.asType(methodType); 58 | } else { 59 | mh = lookup.findSpecial(DynamicObjectInstance.class, "convertAndAssoc", methodType(DynamicObject.class, Object.class, Object.class), proxyType); 60 | mh = MethodHandles.insertArguments(mh, 1, key); 61 | mh = mh.asType(methodType); 62 | } 63 | } else { 64 | Object key = Reflection.getKeyForGetter(method); 65 | if (Reflection.isMetadataGetter(method)) { 66 | mh = lookup.findSpecial(DynamicObjectInstance.class, "getMetadataFor", methodType(Object.class, Object.class), proxyType); 67 | mh = MethodHandles.insertArguments(mh, 1, key); 68 | mh = mh.asType(methodType); 69 | } else { 70 | boolean isRequired = Reflection.isRequired(method); 71 | Type genericReturnType = method.getGenericReturnType(); 72 | mh = lookup.findSpecial(DynamicObjectInstance.class, "invokeGetter", methodType(Object.class, Object.class, boolean.class, Type.class), proxyType); 73 | mh = MethodHandles.insertArguments(mh, 1, key, isRequired, genericReturnType); 74 | mh = mh.asType(methodType); 75 | } 76 | } 77 | } 78 | return new ConstantCallSite(mh); 79 | } 80 | 81 | private boolean isBuilderMethod(Method method) { 82 | return method.getReturnType().equals(dynamicObjectType) && method.getParameterCount() == 1; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/internal/Numerics.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject.internal; 2 | 3 | import java.math.BigInteger; 4 | import java.util.Collections; 5 | import java.util.HashMap; 6 | import java.util.HashSet; 7 | import java.util.Map; 8 | import java.util.Set; 9 | 10 | import static com.github.rschmitt.dynamicobject.internal.ClojureStuff.Bigint; 11 | import static com.github.rschmitt.dynamicobject.internal.ClojureStuff.Biginteger; 12 | 13 | /* 14 | * This class deals with the numeric types that need to be converted to and from long/double/clojure.lang.BigInt. 15 | */ 16 | public class Numerics { 17 | private static final Set> numericTypes; 18 | private static final Map, Class> numericConversions; 19 | private static final Class BigInt = Bigint.invoke(0).getClass(); 20 | 21 | static { 22 | Set> types = new HashSet<>(); 23 | types.add(int.class); 24 | types.add(Integer.class); 25 | types.add(float.class); 26 | types.add(Float.class); 27 | types.add(short.class); 28 | types.add(Short.class); 29 | types.add(byte.class); 30 | types.add(Byte.class); 31 | types.add(BigInteger.class); 32 | numericTypes = Collections.unmodifiableSet(types); 33 | 34 | Map, Class> conversions = new HashMap<>(); 35 | conversions.put(Byte.class, Long.class); 36 | conversions.put(Short.class, Long.class); 37 | conversions.put(Integer.class, Long.class); 38 | conversions.put(Float.class, Double.class); 39 | conversions.put(BigInteger.class, BigInt); 40 | numericConversions = conversions; 41 | } 42 | 43 | static boolean isNumeric(Class type) { 44 | return numericTypes.contains(type); 45 | } 46 | 47 | static Object maybeDownconvert(Class type, Object val) { 48 | if (val == null) return null; 49 | if (type.equals(int.class) || type.equals(Integer.class)) return ((Long) val).intValue(); 50 | if (type.equals(float.class) || type.equals(Float.class)) return ((Double) val).floatValue(); 51 | if (type.equals(short.class) || type.equals(Short.class)) return ((Long) val).shortValue(); 52 | if (type.equals(byte.class) || type.equals(Byte.class)) return ((Long) val).byteValue(); 53 | if (type.equals(BigInt)) return Biginteger.invoke(val); 54 | return val; 55 | } 56 | 57 | static Object maybeUpconvert(Object val) { 58 | if (val instanceof Float) return Double.parseDouble(String.valueOf(val)); 59 | else if (val instanceof Short) return (long) ((short) val); 60 | else if (val instanceof Byte) return (long) ((byte) val); 61 | else if (val instanceof Integer) return (long) ((int) val); 62 | else if (val instanceof BigInteger) return Bigint.invoke(val); 63 | return val; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/internal/Primitives.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject.internal; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /* 7 | * This class deals with the primitive types that need to be boxed and unboxed. 8 | */ 9 | class Primitives { 10 | private static final Map, Class> unboxedToBoxed; 11 | 12 | static { 13 | Map, Class> mapping = new HashMap<>(); 14 | mapping.put(boolean.class, Boolean.class); 15 | mapping.put(char.class, Character.class); 16 | mapping.put(byte.class, Byte.class); 17 | mapping.put(short.class, Short.class); 18 | mapping.put(int.class, Integer.class); 19 | mapping.put(long.class, Long.class); 20 | mapping.put(float.class, Float.class); 21 | mapping.put(double.class, Double.class); 22 | unboxedToBoxed = mapping; 23 | } 24 | 25 | static Class box(Class type) { 26 | return unboxedToBoxed.getOrDefault(type, type); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/internal/RecordReader.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject.internal; 2 | 3 | import clojure.lang.AFn; 4 | import com.github.rschmitt.dynamicobject.DynamicObject; 5 | 6 | import java.util.Map; 7 | 8 | public final class RecordReader> extends AFn { 9 | private final Class type; 10 | 11 | RecordReader(Class type) { 12 | this.type = type; 13 | } 14 | 15 | /** 16 | * For use by clojure.edn/read only. Do not call directly. 17 | */ 18 | @Override 19 | @SuppressWarnings("deprecation") 20 | public Object invoke(Object map) { 21 | return DynamicObject.wrap((Map) map, type).afterDeserialization(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/internal/Reflection.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject.internal; 2 | 3 | import com.github.rschmitt.dynamicobject.Cached; 4 | import com.github.rschmitt.dynamicobject.DynamicObject; 5 | import com.github.rschmitt.dynamicobject.Key; 6 | import com.github.rschmitt.dynamicobject.Meta; 7 | import com.github.rschmitt.dynamicobject.Required; 8 | 9 | import java.lang.annotation.Annotation; 10 | import java.lang.reflect.Method; 11 | import java.lang.reflect.Modifier; 12 | import java.lang.reflect.ParameterizedType; 13 | import java.lang.reflect.Type; 14 | import java.util.Arrays; 15 | import java.util.Collection; 16 | import java.util.LinkedHashSet; 17 | import java.util.List; 18 | import java.util.Set; 19 | import java.util.stream.Stream; 20 | 21 | import static com.github.rschmitt.dynamicobject.internal.ClojureStuff.cachedRead; 22 | import static java.util.stream.Collectors.toSet; 23 | 24 | class Reflection { 25 | static > Collection requiredFields(Class type) { 26 | Collection fields = fieldGetters(type); 27 | return fields.stream().filter(Reflection::isRequired).collect(toSet()); 28 | } 29 | 30 | static > Set cachedKeys(Class type) { 31 | return Arrays.stream(type.getMethods()) 32 | .flatMap(Reflection::getCachedKeysForMethod) 33 | .collect(toSet()); 34 | } 35 | 36 | private static Stream getCachedKeysForMethod(Method method) { 37 | if (isGetter(method)) { 38 | if (method.getAnnotation(Cached.class) != null) { 39 | return Stream.of(getKeyForGetter(method)); 40 | } else { 41 | return Stream.empty(); 42 | } 43 | } else if (isBuilder(method)) { 44 | if (method.getAnnotation(Cached.class) != null) { 45 | return Stream.of(getKeyForBuilder(method)); 46 | } else { 47 | // If the getter has an annotation it'll contribute it directly 48 | return Stream.empty(); 49 | } 50 | } else { 51 | return Stream.empty(); 52 | } 53 | } 54 | 55 | static > Collection fieldGetters(Class type) { 56 | Collection ret = new LinkedHashSet<>(); 57 | for (Method method : type.getDeclaredMethods()) 58 | if (isGetter(method)) 59 | ret.add(method); 60 | return ret; 61 | } 62 | 63 | private static boolean isBuilder(Method method) { 64 | return method.getParameterCount() == 1 && method.getDeclaringClass().isAssignableFrom(method.getReturnType()); 65 | } 66 | 67 | private static boolean isAnyGetter(Method method) { 68 | return method.getParameterCount() == 0 && !method.isDefault() && !method.isSynthetic() 69 | && (method.getModifiers() & Modifier.STATIC) == 0 70 | && method.getReturnType() != Void.TYPE; 71 | } 72 | 73 | private static boolean isGetter(Method method) { 74 | return isAnyGetter(method) && !isMetadataGetter(method); 75 | } 76 | 77 | static boolean isMetadataGetter(Method method) { 78 | return isAnyGetter(method) && hasAnnotation(method, Meta.class); 79 | } 80 | 81 | static boolean isRequired(Method getter) { 82 | return hasAnnotation(getter, Required.class); 83 | } 84 | 85 | private static boolean hasAnnotation(Method method, Class ann) { 86 | List annotations = Arrays.asList(method.getAnnotations()); 87 | for (Annotation annotation : annotations) 88 | if (annotation.annotationType().equals(ann)) 89 | return true; 90 | return false; 91 | } 92 | 93 | static boolean isMetadataBuilder(Method method) { 94 | if (method.getParameterCount() != 1) 95 | return false; 96 | if (hasAnnotation(method, Meta.class)) 97 | return true; 98 | if (hasAnnotation(method, Key.class)) 99 | return false; 100 | Method correspondingGetter = getCorrespondingGetter(method); 101 | return hasAnnotation(correspondingGetter, Meta.class); 102 | } 103 | 104 | static Object getKeyForGetter(Method method) { 105 | Key annotation = getMethodAnnotation(method, Key.class); 106 | if (annotation == null) 107 | return stringToKey(":" + method.getName()); 108 | else 109 | return stringToKey(annotation.value()); 110 | } 111 | 112 | private static Object stringToKey(String keyName) { 113 | if (keyName.charAt(0) == ':') 114 | return cachedRead(keyName); 115 | else 116 | return keyName; 117 | } 118 | 119 | @SuppressWarnings("unchecked") 120 | private static T getMethodAnnotation(Method method, Class annotationType) { 121 | for (Annotation annotation : method.getAnnotations()) 122 | if (annotation.annotationType().equals(annotationType)) 123 | return (T) annotation; 124 | return null; 125 | } 126 | 127 | static Object getKeyForBuilder(Method method) { 128 | Key annotation = getMethodAnnotation(method, Key.class); 129 | if (annotation == null) 130 | return getKeyForGetter(getCorrespondingGetter(method)); 131 | else 132 | return stringToKey(annotation.value()); 133 | } 134 | 135 | private static Method getCorrespondingGetter(Method builderMethod) { 136 | try { 137 | Class type = builderMethod.getDeclaringClass(); 138 | Method correspondingGetter = type.getMethod(builderMethod.getName()); 139 | return correspondingGetter; 140 | } catch (NoSuchMethodException ex) { 141 | throw new IllegalStateException("Builder method " + builderMethod + " must have a corresponding getter method or a @Key annotation.", ex); 142 | } 143 | } 144 | 145 | static Class getRawType(Type type) { 146 | if (type instanceof Class) 147 | return (Class) type; 148 | else if (type instanceof ParameterizedType) { 149 | ParameterizedType parameterizedType = (ParameterizedType) type; 150 | return (Class) parameterizedType.getRawType(); 151 | } else 152 | throw new UnsupportedOperationException(); 153 | } 154 | 155 | static Type getTypeArgument(Type type, int idx) { 156 | if (type instanceof Class) 157 | return Object.class; 158 | else if (type instanceof ParameterizedType) { 159 | ParameterizedType parameterizedType = (ParameterizedType) type; 160 | return parameterizedType.getActualTypeArguments()[idx]; 161 | } else 162 | throw new UnsupportedOperationException(); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/internal/Serialization.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject.internal; 2 | 3 | import java.io.EOFException; 4 | import java.io.IOException; 5 | import java.util.Iterator; 6 | import java.util.NoSuchElementException; 7 | 8 | import com.github.rschmitt.dynamicobject.DynamicObject; 9 | 10 | public class Serialization { 11 | @SuppressWarnings("unchecked") 12 | public static synchronized void deregisterType(Class type) { 13 | EdnSerialization.deregisterType(type); 14 | FressianSerialization.deregisterType(type); 15 | } 16 | 17 | public static synchronized > void registerTag(Class type, String tag) { 18 | EdnSerialization.registerTag(type, tag); 19 | FressianSerialization.registerTag(type, tag); 20 | } 21 | 22 | public static synchronized > void deregisterTag(Class type) { 23 | EdnSerialization.deregisterTag(type); 24 | FressianSerialization.deregisterTag(type); 25 | } 26 | 27 | @FunctionalInterface 28 | interface IOSupplier { 29 | T get() throws IOException; 30 | } 31 | 32 | static Iterator deserializeStreamToIterator(IOSupplier streamReader, Class type) { 33 | return new Iterator() { 34 | private T stash = null; 35 | private boolean done = false; 36 | 37 | @Override 38 | public boolean hasNext() { 39 | populateStash(); 40 | return !done || stash != null; 41 | } 42 | 43 | @Override 44 | public T next() { 45 | if (hasNext()) { 46 | T ret = stash; 47 | stash = null; 48 | return ret; 49 | } else 50 | throw new NoSuchElementException(); 51 | } 52 | 53 | private void populateStash() { 54 | if (stash != null || done) 55 | return; 56 | try { 57 | stash = streamReader.get(); 58 | } catch (NoSuchElementException | EOFException ignore) { 59 | done = true; 60 | } catch (IOException ex) { 61 | throw new RuntimeException(ex); 62 | } 63 | } 64 | }; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/internal/indyproxy/ConcreteMethodTracker.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject.internal.indyproxy; 2 | 3 | import java.lang.reflect.Method; 4 | import java.lang.reflect.Modifier; 5 | import java.util.HashSet; 6 | 7 | class ConcreteMethodTracker { 8 | private HashSet contributors = new HashSet<>(); 9 | 10 | public void add(Method m) { 11 | if ((m.getModifiers() & Modifier.ABSTRACT) != 0) return; 12 | 13 | // Remove any concrete implementations that come from superclasses of the class that owns this new method 14 | // (this new class shadows them). 15 | contributors.removeIf(m2 -> m2.getDeclaringClass().isAssignableFrom(m.getDeclaringClass())); 16 | 17 | // Conversely, if this new implementation is shadowed by any existing implementations, we'll drop it instead 18 | if (contributors.stream().anyMatch(m2 -> m.getDeclaringClass().isAssignableFrom(m2.getDeclaringClass()))) { 19 | return; 20 | } 21 | 22 | contributors.add(m); 23 | } 24 | 25 | public Method getOnlyContributor() { 26 | if (contributors.size() != 1) return null; 27 | 28 | return contributors.iterator().next(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/internal/indyproxy/DefaultInvocationHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject.internal.indyproxy; 2 | 3 | import java.lang.invoke.*; 4 | 5 | class DefaultInvocationHandler implements DynamicInvocationHandler { 6 | private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); 7 | private static final MethodHandle THROW_UNSUPPORTED; 8 | 9 | static { 10 | try { 11 | THROW_UNSUPPORTED = LOOKUP.findStatic(DefaultInvocationHandler.class, 12 | "throwUnsupported", 13 | MethodType.methodType(Object.class)); 14 | } catch (Exception e) { 15 | throw new Error(e); 16 | } 17 | } 18 | 19 | private static Object throwUnsupported() { 20 | throw new UnsupportedOperationException(); 21 | } 22 | 23 | @Override 24 | public CallSite handleInvocation( 25 | MethodHandles.Lookup proxyLookup, 26 | String methodName, 27 | MethodType methodType, 28 | MethodHandle superMethod 29 | ) { 30 | if (superMethod != null) { 31 | return new ConstantCallSite(superMethod.asType(methodType)); 32 | } 33 | 34 | return new ConstantCallSite( 35 | MethodHandles.dropArguments(THROW_UNSUPPORTED, 0, methodType.parameterList()).asType(methodType) 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/internal/indyproxy/DynamicInvocationHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject.internal.indyproxy; 2 | 3 | import java.lang.invoke.CallSite; 4 | import java.lang.invoke.MethodHandle; 5 | import java.lang.invoke.MethodHandles; 6 | import java.lang.invoke.MethodType; 7 | 8 | public interface DynamicInvocationHandler { 9 | /** 10 | * This callback is invoked the first time a proxy method is invoked. It should return a CallSite to be bound to the 11 | * proxy method in question. The CallSite's method arguments (which can also be inspected via methodType) will 12 | * consist of the arguments of the interface or superclass method in question, plus a prepended argument for the 13 | * proxy itself. 14 | *
15 | * Note that the type of the method bound to the callsite must exactly match methodType. Using 16 | * {@link java.lang.invoke.MethodHandle#asType} is recommended. 17 | *
18 | * If multiple calls race, it's possible this method may be invoked multiple times for the same method. In this case, 19 | * one of the returns will be selected arbitrarily and used for all calls. 20 | * 21 | * @param proxyLookup A Lookup instance with the access rights of the proxy class. This can be used to look up 22 | * proxy superclass methods. 23 | * @param methodName The name of the proxy method that is being invoked 24 | * @param methodType The type of the callee (same as the type of the proxy method, but with the proxy instance 25 | * itself prepended) 26 | * @param superMethod If an unambiguous supermethod was found, this has a handle to that supermethod. Otherwise, 27 | * this is null. 28 | * @return A call site to bind to the proxy method. 29 | */ 30 | public CallSite handleInvocation(MethodHandles.Lookup proxyLookup, String methodName, MethodType methodType, MethodHandle superMethod) 31 | throws Throwable; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/rschmitt/dynamicobject/internal/indyproxy/MethodIdentifier.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject.internal.indyproxy; 2 | 3 | import org.objectweb.asm.Type; 4 | 5 | import java.lang.reflect.Method; 6 | import java.util.Arrays; 7 | 8 | class MethodIdentifier { 9 | private final String name; 10 | private final Class returnType; 11 | private final Class[] args; 12 | 13 | public static MethodIdentifier create(Method m) { 14 | return new MethodIdentifier(m.getName(), m.getReturnType(), m.getParameterTypes()); 15 | } 16 | 17 | public MethodIdentifier(String name, Class returnType, Class[] args) { 18 | this.name = name; 19 | this.returnType = returnType; 20 | this.args = args.clone(); 21 | } 22 | 23 | public String getName() { 24 | return name; 25 | } 26 | 27 | public String getDescriptor() { 28 | Type[] typeArgs = new Type[args.length]; 29 | for (int i = 0; i < args.length; i++) { 30 | typeArgs[i] = Type.getType(args[i]); 31 | } 32 | 33 | return Type.getMethodType( 34 | Type.getType(returnType), 35 | typeArgs 36 | ).getDescriptor(); 37 | } 38 | 39 | public Class[] getArgs() { 40 | return args.clone(); 41 | } 42 | 43 | public Class getReturnType() { 44 | return returnType; 45 | } 46 | 47 | @Override 48 | public boolean equals(Object o) { 49 | if (this == o) return true; 50 | if (o == null || getClass() != o.getClass()) return false; 51 | 52 | MethodIdentifier that = (MethodIdentifier) o; 53 | 54 | if (!Arrays.equals(args, that.args)) return false; 55 | if (!name.equals(that.name)) return false; 56 | if (!returnType.equals(that.returnType)) return false; 57 | 58 | return true; 59 | } 60 | 61 | @Override 62 | public int hashCode() { 63 | int result = name.hashCode(); 64 | result = 31 * result + returnType.hashCode(); 65 | result = 31 * result + Arrays.hashCode(args); 66 | return result; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/AcceptanceTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import static java.util.UUID.randomUUID; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | 6 | import java.util.Date; 7 | import java.util.LinkedHashSet; 8 | import java.util.Set; 9 | import java.util.UUID; 10 | 11 | import org.junit.jupiter.api.AfterEach; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.Test; 14 | 15 | public class AcceptanceTest { 16 | @BeforeEach 17 | public void setup() { 18 | DynamicObject.registerType(Path.class, new PathTranslator()); 19 | } 20 | 21 | @AfterEach 22 | public void teardown() { 23 | DynamicObject.deregisterType(Path.class); 24 | } 25 | 26 | @Test 27 | public void acceptanceTest() { 28 | Document document = DynamicObject.newInstance(Document.class); 29 | roundTrip(document); 30 | 31 | document = document.name("Mr. Show").uuid(randomUUID()).date(new Date()); 32 | roundTrip(document); 33 | 34 | document = document.documentPointer(DynamicObject.deserialize("{:location \"/prod-bucket/home\"}", DocumentPointer.class)); 35 | roundTrip(document); 36 | 37 | Set paths = new LinkedHashSet<>(); 38 | paths.add(newPath()); 39 | paths.add(newPath()); 40 | paths.add(newPath()); 41 | document = document.paths(paths); 42 | roundTrip(document); 43 | } 44 | 45 | private void roundTrip(Document document) { 46 | assertEquals(document, DynamicObject.deserialize(DynamicObject.serialize(document), Document.class)); 47 | } 48 | 49 | private Path newPath() { 50 | return new Path(randomString(), randomString(), randomString()); 51 | } 52 | 53 | private String randomString() { 54 | return randomUUID().toString().substring(0, 8); 55 | } 56 | 57 | public interface DocumentPointer extends DynamicObject { 58 | String location(); 59 | 60 | DocumentPointer location(String location); 61 | } 62 | 63 | public interface Document extends DynamicObject { 64 | UUID uuid(); 65 | String name(); 66 | Date date(); 67 | Set paths(); 68 | @Key(":document-pointer") DocumentPointer documentPointer(); 69 | 70 | Document uuid(UUID uuid); 71 | Document name(String name); 72 | Document date(Date date); 73 | Document paths(Set paths); 74 | Document documentPointer(DocumentPointer documentPointer); 75 | } 76 | } 77 | 78 | class Path { 79 | private final String a; 80 | private final String b; 81 | private final String c; 82 | 83 | Path(String a, String b, String c) { 84 | this.a = a; 85 | this.b = b; 86 | this.c = c; 87 | } 88 | 89 | public String getA() { 90 | return a; 91 | } 92 | 93 | public String getB() { 94 | return b; 95 | } 96 | 97 | public String getC() { 98 | return c; 99 | } 100 | 101 | @Override 102 | public boolean equals(Object o) { 103 | if (this == o) return true; 104 | if (o == null || getClass() != o.getClass()) return false; 105 | 106 | Path path = (Path) o; 107 | 108 | if (a != null ? !a.equals(path.a) : path.a != null) return false; 109 | if (b != null ? !b.equals(path.b) : path.b != null) return false; 110 | if (c != null ? !c.equals(path.c) : path.c != null) return false; 111 | 112 | return true; 113 | } 114 | 115 | @Override 116 | public int hashCode() { 117 | int result = a != null ? a.hashCode() : 0; 118 | result = 31 * result + (b != null ? b.hashCode() : 0); 119 | result = 31 * result + (c != null ? c.hashCode() : 0); 120 | return result; 121 | } 122 | 123 | @Override 124 | public String toString() { 125 | return "Path{" + 126 | "a='" + a + '\'' + 127 | ", b='" + b + '\'' + 128 | ", c='" + c + '\'' + 129 | '}'; 130 | } 131 | } 132 | 133 | class PathTranslator implements EdnTranslator { 134 | @Override 135 | public Path read(Object obj) { 136 | String str = (String) obj; 137 | String[] split = str.split("\\/"); 138 | return new Path(split[0], split[1], split[2]); 139 | } 140 | 141 | @Override 142 | public String write(Path obj) { 143 | return String.format("\"%s/%s/%s\"", obj.getA(), obj.getB(), obj.getC()); 144 | } 145 | 146 | @Override 147 | public String getTag() { 148 | return "Path"; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/BuilderTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import static com.github.rschmitt.dynamicobject.TestUtils.assertEquivalent; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | import clojure.lang.PersistentHashMap; 9 | 10 | public class BuilderTest { 11 | @Test 12 | public void createEmptyInstance() { 13 | Buildable obj = DynamicObject.newInstance(Buildable.class); 14 | assertEquals(PersistentHashMap.EMPTY, obj.getMap()); 15 | assertEquals("{}", DynamicObject.serialize(obj)); 16 | } 17 | 18 | @Test 19 | public void invokeBuilderMethod() { 20 | Buildable obj = DynamicObject.newInstance(Buildable.class).str("string"); 21 | assertEquals("{:str \"string\"}", DynamicObject.serialize(obj)); 22 | assertEquals("string", obj.str()); 23 | } 24 | 25 | @Test 26 | public void invokeBuilderWithPrimitive() { 27 | Buildable obj = DynamicObject.newInstance(Buildable.class).i(4).s((short) 127).l(Long.MAX_VALUE).d(3.14).f((float) 3.14); 28 | assertEquivalent("{:f 3.14, :d 3.14, :l 9223372036854775807, :s 127, :i 4}", DynamicObject.serialize(obj)); 29 | assertEquals(4, obj.i()); 30 | assertEquals(127, obj.s()); 31 | assertEquals(Long.MAX_VALUE, obj.l()); 32 | assertEquals(3.14, obj.f(), 0.00001); 33 | assertEquals(3.14, obj.d(), 0.00001); 34 | } 35 | 36 | @Test 37 | public void invokeBuilderWithNull() { 38 | Buildable obj = DynamicObject.newInstance(Buildable.class).str(null); 39 | assertEquals("{:str nil}", DynamicObject.serialize(obj)); 40 | } 41 | 42 | public interface Buildable extends DynamicObject { 43 | String str(); 44 | int i(); 45 | long l(); 46 | short s(); 47 | float f(); 48 | double d(); 49 | 50 | Buildable str(String str); 51 | Buildable i(int i); 52 | Buildable l(long l); 53 | Buildable s(short s); 54 | Buildable f(float f); 55 | Buildable d(double d); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/CollectionsTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import static com.github.rschmitt.dynamicobject.DynamicObject.deserialize; 4 | import static com.github.rschmitt.dynamicobject.DynamicObject.newInstance; 5 | import static java.util.Arrays.asList; 6 | import static java.util.stream.Collectors.toList; 7 | import static java.util.stream.Collectors.toMap; 8 | import static java.util.stream.IntStream.range; 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertTrue; 11 | 12 | import java.util.Base64; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.Random; 17 | import java.util.Set; 18 | 19 | import org.junit.jupiter.api.AfterEach; 20 | import org.junit.jupiter.api.BeforeEach; 21 | import org.junit.jupiter.api.Test; 22 | 23 | public class CollectionsTest { 24 | private static final Random Random = new Random(); 25 | private static final Base64.Encoder Encoder = Base64.getEncoder(); 26 | 27 | @BeforeEach 28 | public void setup() { 29 | DynamicObject.registerTag(ListSchema.class, "ls"); 30 | DynamicObject.registerTag(MapSchema.class, "ms"); 31 | DynamicObject.registerTag(SetSchema.class, "ss"); 32 | } 33 | 34 | @AfterEach 35 | public void teardown() { 36 | DynamicObject.deregisterTag(ListSchema.class); 37 | DynamicObject.deregisterTag(MapSchema.class); 38 | DynamicObject.deregisterTag(SetSchema.class); 39 | } 40 | 41 | @Test 42 | public void listOfStrings() { 43 | ListSchema listSchema = deserialize("{:strings [\"one\" \"two\" \"three\"]}", ListSchema.class); 44 | List stringList = listSchema.strings(); 45 | assertEquals("one", stringList.get(0)); 46 | assertEquals("two", stringList.get(1)); 47 | assertEquals("three", stringList.get(2)); 48 | binaryRoundTrip(listSchema); 49 | } 50 | 51 | // This is just here to prove a point about Java<->Clojure interop. 52 | @Test 53 | public void listStream() { 54 | ListSchema listSchema = deserialize("{:strings [\"one\" \"two\" \"three\"]}", ListSchema.class); 55 | List stringList = listSchema.strings(); 56 | 57 | List collect = stringList.stream().map(x -> x.length()).collect(toList()); 58 | 59 | assertEquals(3, collect.get(0).intValue()); 60 | assertEquals(3, collect.get(1).intValue()); 61 | assertEquals(5, collect.get(2).intValue()); 62 | binaryRoundTrip(listSchema); 63 | } 64 | 65 | @Test 66 | public void setOfStrings() { 67 | SetSchema setSchema = deserialize("{:strings #{\"one\" \"two\" \"three\"}}", SetSchema.class); 68 | Set stringSet = setSchema.strings(); 69 | assertEquals(3, stringSet.size()); 70 | assertTrue(stringSet.contains("one")); 71 | assertTrue(stringSet.contains("two")); 72 | assertTrue(stringSet.contains("three")); 73 | binaryRoundTrip(setSchema); 74 | } 75 | 76 | @Test 77 | public void embeddedMap() { 78 | String edn = "{:dictionary {\"key\" \"value\"}}"; 79 | MapSchema mapSchema = deserialize(edn, MapSchema.class); 80 | assertEquals("value", mapSchema.dictionary().get("key")); 81 | assertEquals(1, mapSchema.dictionary().size()); 82 | binaryRoundTrip(mapSchema); 83 | } 84 | 85 | @Test 86 | public void listOfIntegers() { 87 | ListSchema deserialized = deserialize("{:ints [nil 2 nil 4 nil]}", ListSchema.class); 88 | List builtList = asList(null, 2, null, 4, null); 89 | 90 | ListSchema built = newInstance(ListSchema.class).ints(builtList); 91 | 92 | assertEquals(builtList, deserialized.ints()); 93 | assertEquals(builtList, built.ints()); 94 | binaryRoundTrip(built); 95 | binaryRoundTrip(deserialized); 96 | } 97 | 98 | @Test 99 | public void mapOfIntegersToIntegers() { 100 | MapSchema deserialized = deserialize("{:ints {1 2, 3 4}}", MapSchema.class); 101 | Map builtMap = new HashMap<>(); 102 | builtMap.put(1, 2); 103 | builtMap.put(3, 4); 104 | MapSchema built = newInstance(MapSchema.class).ints(builtMap); 105 | 106 | assertEquals(builtMap, deserialized.ints()); 107 | assertEquals(builtMap, built.ints()); 108 | binaryRoundTrip(deserialized); 109 | binaryRoundTrip(built); 110 | } 111 | 112 | @Test 113 | public void largeList() { 114 | List strings = range(0, 10_000).mapToObj(n -> string()).collect(toList()); 115 | 116 | ListSchema listSchema = newInstance(ListSchema.class).strings(strings); 117 | 118 | assertEquals(strings, listSchema.strings()); 119 | binaryRoundTrip(listSchema); 120 | } 121 | 122 | @Test 123 | public void largeMap() { 124 | Map map = range(0, 10_000).boxed().collect(toMap(n -> string(), n -> string())); 125 | 126 | MapSchema mapSchema = newInstance(MapSchema.class).dictionary(map); 127 | 128 | assertEquals(map.size(), mapSchema.dictionary().size()); 129 | assertEquals(map, mapSchema.dictionary()); 130 | binaryRoundTrip(mapSchema); 131 | } 132 | 133 | private void binaryRoundTrip(Object expected) { 134 | Object actual = DynamicObject.fromFressianByteArray(DynamicObject.toFressianByteArray(expected)); 135 | assertEquals(expected, actual); 136 | } 137 | 138 | private static String string() { 139 | byte[] buf = new byte[64]; 140 | Random.nextBytes(buf); 141 | return Encoder.encodeToString(buf); 142 | } 143 | 144 | public interface ListSchema extends DynamicObject { 145 | List strings(); 146 | List ints(); 147 | 148 | ListSchema strings(List strings); 149 | ListSchema ints(List ints); 150 | } 151 | 152 | public interface SetSchema extends DynamicObject { 153 | Set strings(); 154 | } 155 | 156 | public interface MapSchema extends DynamicObject { 157 | Map dictionary(); 158 | Map ints(); 159 | 160 | MapSchema dictionary(Map dictionary); 161 | MapSchema ints(Map ints); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/ColliderTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import static com.github.rschmitt.collider.Collider.clojureList; 4 | import static com.github.rschmitt.collider.Collider.clojureMap; 5 | import static com.github.rschmitt.collider.Collider.clojureSet; 6 | import static com.github.rschmitt.dynamicobject.DynamicObject.deserialize; 7 | import static com.github.rschmitt.dynamicobject.DynamicObject.fromFressianByteArray; 8 | import static com.github.rschmitt.dynamicobject.DynamicObject.newInstance; 9 | import static com.github.rschmitt.dynamicobject.DynamicObject.toFressianByteArray; 10 | import static java.util.Optional.empty; 11 | import static java.util.Optional.of; 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertTrue; 14 | 15 | import java.time.Instant; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.Optional; 19 | import java.util.Set; 20 | 21 | import org.junit.jupiter.api.BeforeAll; 22 | import org.junit.jupiter.api.Test; 23 | 24 | import com.github.rschmitt.collider.ClojureList; 25 | import com.github.rschmitt.collider.ClojureMap; 26 | import com.github.rschmitt.collider.ClojureSet; 27 | 28 | public class ColliderTest { 29 | static final Batch emptyBatch = newInstance(Batch.class); 30 | static final Instant inst = Instant.parse("1985-04-12T23:20:50.52Z"); 31 | 32 | @BeforeAll 33 | public static void setup() { 34 | DynamicObject.registerTag(Batch.class, "batch"); 35 | } 36 | 37 | @Test 38 | public void clojureMapDeserialization() throws Exception { 39 | Batch batch = deserialize("{:map {\"key\" 3}}", Batch.class); 40 | 41 | ClojureMap map = batch.map(); 42 | 43 | assertEquals(3, map.get("key").intValue()); 44 | assertTrue(map.dissoc("key").isEmpty()); 45 | fressianRoundTrip(batch); 46 | } 47 | 48 | @Test 49 | public void clojureSetDeserialization() throws Exception { 50 | Batch batch = deserialize("{:set #{#inst \"1985-04-12T23:20:50.520-00:00\"}}", Batch.class); 51 | 52 | ClojureSet set = batch.set(); 53 | 54 | assertTrue(set.contains(inst)); 55 | fressianRoundTrip(batch); 56 | } 57 | 58 | @Test 59 | public void clojureListDeserialization() throws Exception { 60 | Batch batch = deserialize("{:list [\"a\" nil \"c\"]}", Batch.class); 61 | 62 | ClojureList> list = batch.list(); 63 | 64 | assertEquals(of("a"), list.get(0)); 65 | assertEquals(empty(), list.get(1)); 66 | assertEquals(of("c"), list.get(2)); 67 | assertEquals(of("d"), list.append(of("d")).get(3)); 68 | fressianRoundTrip(batch); 69 | } 70 | 71 | @Test 72 | public void clojureMapBuilders() throws Exception { 73 | ClojureMap map = clojureMap("key", 3); 74 | 75 | Batch batch = emptyBatch.map(map); 76 | 77 | assertEquals(map, batch.map()); 78 | fressianRoundTrip(batch); 79 | } 80 | 81 | @Test 82 | public void clojureSetBuilders() throws Exception { 83 | ClojureSet set = clojureSet(inst); 84 | 85 | Batch batch = emptyBatch.set(set); 86 | 87 | assertEquals(set, batch.set()); 88 | fressianRoundTrip(batch); 89 | } 90 | 91 | @Test 92 | public void clojureListBuilders() throws Exception { 93 | ClojureList> list = clojureList(of("a"), empty(), of("c")); 94 | 95 | Batch batch = emptyBatch.list(list); 96 | 97 | assertEquals(list, batch.list()); 98 | fressianRoundTrip(batch); 99 | } 100 | 101 | @Test 102 | public void mapBuilders() throws Exception { 103 | ClojureMap map = clojureMap("key", 3); 104 | 105 | Batch batch = emptyBatch.map2(map); 106 | 107 | assertEquals(map, batch.map()); 108 | fressianRoundTrip(batch); 109 | } 110 | 111 | @Test 112 | public void setBuilders() throws Exception { 113 | ClojureSet set = clojureSet(inst); 114 | 115 | Batch batch = emptyBatch.set2(set); 116 | 117 | assertEquals(set, batch.set()); 118 | fressianRoundTrip(batch); 119 | } 120 | 121 | @Test 122 | public void listBuilders() throws Exception { 123 | ClojureList> list = clojureList(of("a"), empty(), of("c")); 124 | 125 | Batch batch = emptyBatch.list2(list); 126 | 127 | assertEquals(list, batch.list()); 128 | fressianRoundTrip(batch); 129 | } 130 | 131 | private void fressianRoundTrip(Batch batch) { 132 | Batch actual = fromFressianByteArray(toFressianByteArray(batch)); 133 | 134 | assertEquals(batch, actual); 135 | assertEquals(batch.map(), actual.map()); 136 | assertEquals(batch.set(), actual.set()); 137 | assertEquals(batch.list(), actual.list()); 138 | } 139 | 140 | public interface Batch extends DynamicObject { 141 | ClojureMap map(); 142 | ClojureSet set(); 143 | ClojureList> list(); 144 | 145 | Batch map(Map map); 146 | Batch set(Set set); 147 | Batch list(List> list); 148 | 149 | @Key(":map") Batch map2(ClojureMap map); 150 | @Key(":set") Batch set2(ClojureSet set); 151 | @Key(":list") Batch list2(ClojureList> list); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/CustomKeyTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import static com.github.rschmitt.dynamicobject.DynamicObject.deserialize; 4 | import static com.github.rschmitt.dynamicobject.DynamicObject.newInstance; 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class CustomKeyTest { 10 | @Test 11 | public void customKeywordSupport() { 12 | String edn = "{:a-sample-int 5}"; 13 | KeywordInterface object = deserialize(edn, KeywordInterface.class); 14 | assertEquals(5, object.aSampleInt()); 15 | assertEquals(object, newInstance(KeywordInterface.class).aSampleInt(5)); 16 | } 17 | 18 | @Test 19 | public void customStringSupport() { 20 | String edn = "{\"a-sample-string\", \"a-sample-value\"}"; 21 | StringInterface object = deserialize(edn, StringInterface.class); 22 | assertEquals("a-sample-value", object.sampleString()); 23 | assertEquals(object, newInstance(StringInterface.class).sampleString("a-sample-value")); 24 | } 25 | 26 | @Test 27 | public void customBuilderSupport() { 28 | String edn = "{:element \"a string\"}"; 29 | 30 | CustomBuilder expected = deserialize(edn, CustomBuilder.class); 31 | CustomBuilder actual = newInstance(CustomBuilder.class).withElement("a string"); 32 | 33 | assertEquals(expected, actual); 34 | } 35 | 36 | public interface KeywordInterface extends DynamicObject { 37 | @Key(":a-sample-int") int aSampleInt(); 38 | 39 | KeywordInterface aSampleInt(int aSampleInt); 40 | } 41 | 42 | public interface StringInterface extends DynamicObject { 43 | @Key("a-sample-string") String sampleString(); 44 | 45 | StringInterface sampleString(String sampleString); 46 | } 47 | 48 | public interface CustomBuilder extends DynamicObject { 49 | @Key(":element") String getElement(); 50 | @Key(":element") CustomBuilder withElement(String element); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/CustomMethodTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class CustomMethodTest { 8 | @Test 9 | public void invokeCustomMethod() { 10 | CustomMethod obj = DynamicObject.newInstance(CustomMethod.class); 11 | assertEquals("asdf", obj.customMethod()); 12 | } 13 | 14 | @Test 15 | public void invokeGettersFromCustomMethod() { 16 | CustomMethod obj = DynamicObject.newInstance(CustomMethod.class).str("a string"); 17 | assertEquals("a string", obj.callIntoGetter()); 18 | } 19 | 20 | @Test 21 | public void invokeCustomWither() { 22 | CustomMethod obj = DynamicObject.newInstance(CustomMethod.class).customWither(4); 23 | assertEquals("4", obj.callIntoGetter()); 24 | } 25 | 26 | public interface CustomMethod extends DynamicObject { 27 | String str(); 28 | 29 | CustomMethod str(String str); 30 | 31 | default String customMethod() { 32 | return "asdf"; 33 | } 34 | 35 | default String callIntoGetter() { 36 | return str(); 37 | } 38 | 39 | default CustomMethod customWither(int x) { 40 | return str(String.valueOf(x)); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/DefaultReaderTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import clojure.java.api.Clojure; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | 9 | import static com.github.rschmitt.dynamicobject.DynamicObject.serialize; 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | import static org.junit.jupiter.api.Assertions.assertThrows; 12 | 13 | @SuppressWarnings("rawtypes") 14 | public class DefaultReaderTest { 15 | @Test 16 | public void testUnknownReader() { 17 | String edn = "#some-namespace/some-record-name{:key :value}"; 18 | Object obj = DynamicObject.deserialize(edn, Object.class); 19 | Unknown unknown = (Unknown) obj; 20 | 21 | assertEquals("some-namespace/some-record-name", unknown.getTag()); 22 | assertEquals(Clojure.read("{:key :value}"), unknown.getElement()); 23 | assertEquals(serialize(unknown), edn); 24 | } 25 | 26 | @Test 27 | public void disableUnknownReader() { 28 | DynamicObject.setDefaultReader(null); 29 | assertThrows(RuntimeException.class, () -> DynamicObject.deserialize("#unknown{}", Object.class)); 30 | DynamicObject.setDefaultReader(Unknown::new); 31 | } 32 | 33 | @Test 34 | public void testUnknownSerialization() { 35 | Unknown map = new Unknown("tag", new HashMap()); 36 | Unknown str = new Unknown("tag", "asdf"); 37 | Unknown vec = new Unknown("tag", new ArrayList()); 38 | 39 | assertEquals("#tag{}", serialize(map)); 40 | assertEquals("#tag \"asdf\"", serialize(str)); 41 | assertEquals("#tag []", serialize(vec)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/DeserializationHookTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import org.junit.jupiter.api.BeforeAll; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static com.github.rschmitt.dynamicobject.DynamicObject.*; 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | @SuppressWarnings("deprecation") 10 | public class DeserializationHookTest { 11 | @BeforeAll 12 | public static void setup() { 13 | registerTag(Registered.class, "Reg"); 14 | } 15 | 16 | @Test 17 | public void ednDeserialization() { 18 | Registered r = (Registered) deserialize("#Reg{}", Object.class); 19 | 20 | assertEquals(42L, r.value()); 21 | } 22 | 23 | @Test 24 | public void fressianDeserialization() throws Exception { 25 | Registered oldVersion = newInstance(Registered.class); 26 | 27 | byte[] bytes = toFressianByteArray(oldVersion); 28 | Registered newVersion = fromFressianByteArray(bytes); 29 | 30 | assertEquals(42, newVersion.value()); 31 | } 32 | 33 | @Test 34 | public void hintedEdnDeserialization() throws Exception { 35 | Unregistered u = deserialize("{}", Unregistered.class); 36 | assertEquals(42L, u.value()); 37 | } 38 | 39 | public interface Registered extends DynamicObject { 40 | long value(); 41 | 42 | Registered value(long value); 43 | 44 | @Override 45 | default Registered afterDeserialization() { 46 | return value(42); 47 | } 48 | } 49 | 50 | public interface Unregistered extends DynamicObject { 51 | long value(); 52 | 53 | Unregistered value(long value); 54 | 55 | @Override 56 | default Unregistered afterDeserialization() { 57 | return value(42); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/DiffTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import static com.github.rschmitt.dynamicobject.DynamicObject.deserialize; 4 | import static com.github.rschmitt.dynamicobject.DynamicObject.serialize; 5 | import static com.github.rschmitt.dynamicobject.TestUtils.assertEquivalent; 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | import static org.junit.jupiter.api.Assertions.assertNotEquals; 8 | import static org.junit.jupiter.api.Assertions.assertNull; 9 | 10 | import java.util.List; 11 | import java.util.Set; 12 | 13 | import org.junit.jupiter.api.AfterEach; 14 | import org.junit.jupiter.api.BeforeEach; 15 | import org.junit.jupiter.api.Test; 16 | 17 | public class DiffTest { 18 | @BeforeEach 19 | public void setup() { 20 | DynamicObject.registerTag(Diffable.class, "D"); 21 | } 22 | 23 | @AfterEach 24 | public void teardown() { 25 | DynamicObject.deregisterTag(Diffable.class); 26 | } 27 | 28 | @Test 29 | public void union() { 30 | Diffable a = deserialize("#D{:a \"a\", :b \"b\"}", Diffable.class); 31 | Diffable b = deserialize("#D{:a \"a\", :b \"b\", :c \"c\"}", Diffable.class); 32 | 33 | Diffable c = a.intersect(b); 34 | 35 | assertEquals(c, a); 36 | assertNotEquals(c, b); 37 | assertEquivalent("#D{:a \"a\", :b \"b\"}", serialize(c)); 38 | } 39 | 40 | @Test 41 | public void emptyUnion() { 42 | Diffable a = deserialize("#D{:a \"a\"}", Diffable.class); 43 | Diffable b = deserialize("#D{:b \"b\"}", Diffable.class); 44 | 45 | Diffable c = a.intersect(b); 46 | 47 | assertNull(c.a()); 48 | assertNull(c.b()); 49 | assertEquals("#D{}", serialize(c)); 50 | } 51 | 52 | @Test 53 | public void mapSubdiff() { 54 | Diffable a = deserialize("#D{:d #D{:a \"inner\"}, :a \"a\", :b \"?\"}", Diffable.class); 55 | Diffable b = deserialize("#D{:d #D{:a \"inner\", :b \"ignored\"}, :a \"a\", :b \"!\"}", Diffable.class); 56 | 57 | Diffable c = a.intersect(b); 58 | 59 | assertEquals("a", c.a()); 60 | assertNull(c.b()); 61 | assertEquals(a.d(), c.d()); 62 | assertEquals("inner", a.d().a()); 63 | } 64 | 65 | @Test 66 | public void setsAreNotSubdiffed() { 67 | Diffable a = deserialize("#D{:s #{1 2 3}}", Diffable.class); 68 | Diffable b = deserialize("#D{:s #{1 2 3 4}}", Diffable.class); 69 | 70 | Diffable c = a.intersect(b); 71 | 72 | assertEquals(null, c.set()); 73 | } 74 | 75 | @Test 76 | public void listsAreSubdiffed() { 77 | Diffable a = deserialize("#D{:list [5 4 3]}", Diffable.class); 78 | Diffable b = deserialize("#D{:list [1 2 3]}", Diffable.class); 79 | 80 | Diffable c = a.intersect(b); 81 | 82 | assertEquals(null, c.list().get(0)); 83 | assertEquals(null, c.list().get(1)); 84 | assertEquals(Integer.valueOf(3), c.list().get(2)); 85 | } 86 | 87 | @Test 88 | public void subtraction() { 89 | Diffable a = deserialize("#D{:a \"same\", :b \"different\", :set #{1 2 3}, :list [1 2 1]}", Diffable.class); 90 | Diffable b = deserialize("#D{:a \"same\", :b \"?????????\", :set #{1 2 3}, :list [1 2 3]}", Diffable.class); 91 | 92 | Diffable diff = a.subtract(b); 93 | 94 | assertNull(diff.a()); 95 | assertNull(diff.set()); 96 | assertEquals("different", diff.b()); 97 | assertNull(diff.list().get(0)); 98 | assertNull(diff.list().get(1)); 99 | assertEquals(Integer.valueOf(1), diff.list().get(2)); 100 | } 101 | 102 | public interface Diffable extends DynamicObject { 103 | String a(); 104 | String b(); 105 | Diffable d(); 106 | Set set(); 107 | List list(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/ExtensibilityTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import static com.github.rschmitt.dynamicobject.DynamicObject.deserialize; 4 | import static com.github.rschmitt.dynamicobject.DynamicObject.fromFressianByteArray; 5 | import static com.github.rschmitt.dynamicobject.DynamicObject.serialize; 6 | import static com.github.rschmitt.dynamicobject.DynamicObject.toFressianByteArray; 7 | import static com.github.rschmitt.dynamicobject.TestUtils.assertEquivalent; 8 | import static java.lang.String.format; 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | 11 | import java.io.IOException; 12 | import java.util.List; 13 | import java.util.Map; 14 | 15 | import org.fressian.Reader; 16 | import org.fressian.Writer; 17 | import org.fressian.handlers.ReadHandler; 18 | import org.fressian.handlers.WriteHandler; 19 | import org.junit.jupiter.api.AfterEach; 20 | import org.junit.jupiter.api.BeforeEach; 21 | import org.junit.jupiter.api.Test; 22 | 23 | public class ExtensibilityTest { 24 | private static final String Edn = "#dh{:dumb [#MyDumbClass{:version 1, :str \"str\"}]}"; 25 | 26 | @BeforeEach 27 | public void setup() { 28 | DynamicObject.registerType(DumbClass.class, new DumbClassTranslator()); 29 | DynamicObject.registerTag(DumbClassHolder.class, "dh"); 30 | DynamicObject.registerType(DumbClass.class, "dumb", new DumbClassReader(), new DumbClassWriter()); 31 | } 32 | 33 | @AfterEach 34 | public void teardown() { 35 | DynamicObject.deregisterType(DumbClass.class); 36 | DynamicObject.deregisterTag(DumbClassHolder.class); 37 | } 38 | 39 | @Test 40 | public void roundTrip() { 41 | DumbClassHolder holder = deserialize(Edn, DumbClassHolder.class); 42 | 43 | String serialized = serialize(holder); 44 | 45 | assertEquivalent(Edn, serialized); 46 | assertEquals(new DumbClass(1, "str"), holder.dumb().get(0)); 47 | assertEquals(holder, fromFressianByteArray(toFressianByteArray(holder))); 48 | } 49 | 50 | @Test 51 | public void serializeRegisteredType() { 52 | DumbClass dumbClass = new DumbClass(24, "twenty-four"); 53 | 54 | String serialized = serialize(dumbClass); 55 | 56 | assertEquivalent("#MyDumbClass{:version 24, :str \"twenty-four\"}", serialized); 57 | } 58 | 59 | @Test 60 | public void deserializeRegisteredType() { 61 | String edn = "#MyDumbClass{:version 24, :str \"twenty-four\"}"; 62 | 63 | DumbClass instance = deserialize(edn, DumbClass.class); 64 | 65 | assertEquals(new DumbClass(24, "twenty-four"), instance); 66 | } 67 | 68 | @Test 69 | public void prettyPrint() { 70 | DumbClassHolder holder = deserialize(Edn, DumbClassHolder.class); 71 | String expectedFormattedString = format("#dh{:dumb [#MyDumbClass{:version 1, :str \"str\"}]}%n"); 72 | assertEquivalent(expectedFormattedString, holder.toFormattedString()); 73 | } 74 | 75 | @Test 76 | public void serializeBuiltinType() { 77 | assertEquals("true", serialize(true)); 78 | assertEquals("false", serialize(false)); 79 | assertEquals("25", serialize(25)); 80 | assertEquals("\"asdf\"", serialize("asdf")); 81 | } 82 | 83 | // This is a DynamicObject that contains a regular POJO. 84 | public interface DumbClassHolder extends DynamicObject { 85 | List dumb(); 86 | } 87 | } 88 | 89 | // This is a translation class that functions as an Edn reader/writer for its associated POJO. 90 | class DumbClassTranslator implements EdnTranslator { 91 | @Override 92 | public DumbClass read(Object obj) { 93 | DumbClassProxy proxy = DynamicObject.wrap((Map) obj, DumbClassProxy.class); 94 | return new DumbClass(proxy.version(), proxy.str()); 95 | } 96 | 97 | @Override 98 | public String write(DumbClass obj) { 99 | DumbClassProxy proxy = DynamicObject.newInstance(DumbClassProxy.class); 100 | proxy = proxy.str(obj.getStr()); 101 | proxy = proxy.version(obj.getVersion()); 102 | return serialize(proxy); 103 | } 104 | 105 | @Override 106 | public String getTag() { 107 | return "MyDumbClass"; // This is deliberately different from the class name. 108 | } 109 | 110 | public interface DumbClassProxy extends DynamicObject { 111 | long version(); 112 | String str(); 113 | 114 | DumbClassProxy version(long version); 115 | DumbClassProxy str(String str); 116 | } 117 | } 118 | 119 | class DumbClassReader implements ReadHandler { 120 | @Override 121 | public Object read(Reader r, Object tag, int componentCount) throws IOException { 122 | return new DumbClass(r.readInt(), (String) r.readObject()); 123 | } 124 | } 125 | 126 | class DumbClassWriter implements WriteHandler { 127 | @Override 128 | public void write(Writer w, Object instance) throws IOException { 129 | DumbClass dumb = (DumbClass) instance; 130 | w.writeTag("dumb", 2); 131 | w.writeInt(dumb.getVersion()); 132 | w.writeObject(dumb.getStr()); 133 | } 134 | } 135 | 136 | // This is a POJO that has no knowledge of Edn. 137 | class DumbClass { 138 | private final long version; 139 | private final String str; 140 | 141 | DumbClass(long version, String str) { 142 | this.version = version; 143 | this.str = str; 144 | } 145 | 146 | public long getVersion() { 147 | return version; 148 | } 149 | 150 | public String getStr() { 151 | return str; 152 | } 153 | 154 | @Override 155 | public boolean equals(Object o) { 156 | if (this == o) return true; 157 | if (o == null || getClass() != o.getClass()) return false; 158 | 159 | DumbClass dumbClass = (DumbClass) o; 160 | 161 | if (version != dumbClass.version) return false; 162 | return str != null ? str.equals(dumbClass.str) : dumbClass.str == null; 163 | } 164 | 165 | @Override 166 | public int hashCode() { 167 | int result = (int) (version ^ (version >>> 32)); 168 | result = 31 * result + (str != null ? str.hashCode() : 0); 169 | return result; 170 | } 171 | 172 | @Override 173 | public String toString() { 174 | throw new UnsupportedOperationException("I'm a useless legacy toString() method that doesn't produce Edn!"); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/FressianTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import java.util.Arrays; 7 | import java.util.Base64; 8 | import java.util.List; 9 | 10 | import org.junit.jupiter.api.AfterEach; 11 | import org.junit.jupiter.api.BeforeEach; 12 | import org.junit.jupiter.api.Test; 13 | 14 | public class FressianTest { 15 | public static final BinarySerialized SAMPLE_VALUE 16 | = DynamicObject.newInstance(BinarySerialized.class).withHello("world"); 17 | 18 | @BeforeEach 19 | public void setup() { 20 | DynamicObject.registerTag(BinarySerialized.class, "BinarySerialized"); 21 | } 22 | 23 | @AfterEach 24 | public void teardown() { 25 | DynamicObject.deregisterTag(BinarySerialized.class); 26 | } 27 | 28 | @Test 29 | public void smokeTest() throws Exception { 30 | byte[] bytes = DynamicObject.toFressianByteArray(SAMPLE_VALUE); 31 | Object o = DynamicObject.fromFressianByteArray(bytes); 32 | 33 | assertEquals(o, SAMPLE_VALUE); 34 | } 35 | 36 | @Test 37 | public void testNullValues() throws Exception { 38 | BinarySerialized testValue = SAMPLE_VALUE.withNull(null); 39 | 40 | byte[] bytes = DynamicObject.toFressianByteArray(testValue); 41 | Object o = DynamicObject.fromFressianByteArray(bytes); 42 | 43 | assertEquals(o, testValue); 44 | } 45 | 46 | @Test 47 | public void formatCompatibilityTest() throws Exception { 48 | String encoded = "7+MQQmluYXJ5U2VyaWFsaXplZAHA5sr3zd9oZWxsb993b3JsZA=="; 49 | BinarySerialized deserialized = DynamicObject.fromFressianByteArray(Base64.getDecoder().decode(encoded)); 50 | 51 | assertEquals(SAMPLE_VALUE, deserialized); 52 | } 53 | 54 | @Test 55 | public void cachedKeys_canBeRoundTripped() throws Exception { 56 | String cachedValue = "cached value"; 57 | BinarySerialized value = DynamicObject.newInstance(BinarySerialized.class).withCached(cachedValue); 58 | 59 | byte[] fressian = DynamicObject.toFressianByteArray(Arrays.asList(value, value)); 60 | List deserialized = DynamicObject.fromFressianByteArray(fressian); 61 | 62 | assertEquals(value, deserialized.get(0)); 63 | assertEquals(value, deserialized.get(1)); 64 | } 65 | 66 | @Test 67 | public void deregisteringAClassRepeatedly_doesNotThrowAnNPE() throws Exception { 68 | // First call to deregister & remove values from internal caches 69 | DynamicObject.deregisterTag(BinarySerialized.class); 70 | 71 | // This call should not throw an exception 72 | DynamicObject.deregisterTag(BinarySerialized.class); 73 | } 74 | 75 | @Test 76 | public void cachedKeys_areNotRepeated() throws Exception { 77 | String cachedValue = "cached value"; 78 | BinarySerialized value = DynamicObject.newInstance(BinarySerialized.class).withCached(cachedValue); 79 | 80 | byte[] fressian = DynamicObject.toFressianByteArray(Arrays.asList(value, value)); 81 | // Interpret as an 8-bit charset just to make it easy to find the embedded string(s) 82 | String s = new String(fressian, "ISO-8859-1"); 83 | 84 | int firstIndex = s.indexOf(cachedValue); 85 | assertTrue(firstIndex >= 0); 86 | 87 | int secondIndex = s.indexOf(cachedValue, firstIndex + 1); 88 | assertEquals(-1, secondIndex); 89 | } 90 | 91 | public interface BinarySerialized extends DynamicObject { 92 | @Key(":hello") BinarySerialized withHello(String hello); 93 | @Key(":null") BinarySerialized withNull(Object nil); 94 | @Cached @Key(":cached") BinarySerialized withCached(String cached); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/InstantTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import static com.github.rschmitt.dynamicobject.DynamicObject.deserialize; 4 | import static com.github.rschmitt.dynamicobject.DynamicObject.newInstance; 5 | import static com.github.rschmitt.dynamicobject.DynamicObject.serialize; 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | 8 | import java.time.Instant; 9 | import java.util.Date; 10 | 11 | import org.junit.jupiter.api.Test; 12 | 13 | public class InstantTest { 14 | @Test 15 | public void dateBuilder() { 16 | Date expected = Date.from(Instant.parse("1985-04-12T23:20:50.52Z")); 17 | 18 | TimeWrapper timeWrapper = newInstance(TimeWrapper.class).date(expected); 19 | 20 | assertEquals(expected, timeWrapper.date()); 21 | assertEquals("{:date #inst \"1985-04-12T23:20:50.520-00:00\"}", serialize(timeWrapper)); 22 | } 23 | 24 | @Test 25 | public void instantBuilder() { 26 | Instant expected = Instant.parse("1985-04-12T23:20:50.52Z"); 27 | 28 | TimeWrapper timeWrapper = newInstance(TimeWrapper.class).instant(expected); 29 | 30 | assertEquals(expected, timeWrapper.instant()); 31 | assertEquals("{:instant #inst \"1985-04-12T23:20:50.520-00:00\"}", serialize(timeWrapper)); 32 | } 33 | 34 | @Test 35 | public void dateParser() { 36 | String edn = "{:date #inst \"1985-04-12T23:20:50.520-00:00\"}"; 37 | Date expected = Date.from(Instant.parse("1985-04-12T23:20:50.52Z")); 38 | 39 | TimeWrapper timeWrapper = deserialize(edn, TimeWrapper.class); 40 | 41 | assertEquals(expected, timeWrapper.date()); 42 | } 43 | 44 | @Test 45 | public void instantParser() { 46 | String edn = "{:instant #inst \"1985-04-12T23:20:50.520-00:00\"}"; 47 | Instant expected = Instant.parse("1985-04-12T23:20:50.52Z"); 48 | 49 | TimeWrapper timeWrapper = deserialize(edn, TimeWrapper.class); 50 | 51 | assertEquals(expected, timeWrapper.instant()); 52 | assertEquals(edn, serialize(timeWrapper)); 53 | } 54 | 55 | public interface TimeWrapper extends DynamicObject { 56 | Date date(); 57 | Instant instant(); 58 | 59 | TimeWrapper date(Date date); 60 | TimeWrapper instant(Instant instant); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/MapTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import clojure.lang.EdnReader; 4 | import clojure.lang.PersistentHashMap; 5 | import org.junit.jupiter.api.AfterEach; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import static com.github.rschmitt.dynamicobject.DynamicObject.deserialize; 10 | import static com.github.rschmitt.dynamicobject.DynamicObject.serialize; 11 | import static java.lang.String.format; 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertFalse; 14 | 15 | @SuppressWarnings("unchecked") 16 | public class MapTest { 17 | static final String SimpleEdn = "{:str \"expected value\", :i 4, :d 3.14}"; 18 | static final String NestedEdn = format("{:version 1, :simple %s}", SimpleEdn); 19 | static final String TaggedEdn = format("#eo%s", NestedEdn); 20 | 21 | @BeforeEach 22 | public void setup() { 23 | DynamicObject.registerTag(EmptyObject.class, "eo"); 24 | } 25 | 26 | @AfterEach 27 | public void teardown() { 28 | DynamicObject.deregisterTag(EmptyObject.class); 29 | } 30 | 31 | @Test 32 | public void getMapReturnsBackingMap() { 33 | EmptyObject object = deserialize(TaggedEdn, EmptyObject.class); 34 | Object map = EdnReader.readString(NestedEdn, PersistentHashMap.EMPTY); 35 | assertEquals(map, object.getMap()); 36 | binaryRoundTrip(object); 37 | } 38 | 39 | @Test 40 | public void unknownFieldsAreConsideredForEquality() { 41 | EmptyObject obj1 = deserialize(SimpleEdn, EmptyObject.class); 42 | EmptyObject obj2 = deserialize(NestedEdn, EmptyObject.class); 43 | assertFalse(obj1.equals(obj2)); 44 | binaryRoundTrip(obj1); 45 | binaryRoundTrip(obj2); 46 | } 47 | 48 | @Test 49 | public void unknownFieldsAreSerialized() { 50 | EmptyObject nestedObj = deserialize(TaggedEdn, EmptyObject.class); 51 | String actualEdn = serialize(nestedObj); 52 | assertEquals(TaggedEdn, actualEdn); 53 | } 54 | 55 | @Test 56 | public void mapDefaultMethodsAreUsable() throws Exception { 57 | EmptyObject object = DynamicObject.newInstance(EmptyObject.class); 58 | 59 | object.getOrDefault("some key", "some value"); 60 | } 61 | 62 | private void binaryRoundTrip(Object expected) { 63 | Object actual = DynamicObject.fromFressianByteArray(DynamicObject.toFressianByteArray(expected)); 64 | assertEquals(expected, actual); 65 | } 66 | 67 | public interface EmptyObject extends DynamicObject { 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/MergeTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import static com.github.rschmitt.dynamicobject.TestUtils.assertEquivalent; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | 6 | import org.junit.jupiter.api.AfterEach; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.Test; 9 | 10 | public class MergeTest { 11 | @BeforeEach 12 | public void setup() { 13 | DynamicObject.registerTag(Mergeable.class, "M"); 14 | } 15 | 16 | @AfterEach 17 | public void teardown() { 18 | DynamicObject.deregisterTag(Mergeable.class); 19 | } 20 | 21 | @Test 22 | public void twoEmptyObjects() { 23 | Mergeable a = DynamicObject.deserialize("#M{:a nil}", Mergeable.class); 24 | Mergeable b = DynamicObject.deserialize("#M{:a nil}", Mergeable.class); 25 | 26 | Mergeable c = a.merge(b); 27 | 28 | assertEquals("#M{:a nil}", DynamicObject.serialize(c)); 29 | } 30 | 31 | @Test 32 | public void twoFullObjects() { 33 | Mergeable a = DynamicObject.deserialize("#M{:a \"first\"}", Mergeable.class); 34 | Mergeable b = DynamicObject.deserialize("#M{:a \"second\"}", Mergeable.class); 35 | 36 | Mergeable c = a.merge(b); 37 | 38 | assertEquals("#M{:a \"second\"}", DynamicObject.serialize(c)); 39 | } 40 | 41 | @Test 42 | public void nullsDoNotReplaceNonNulls() { 43 | Mergeable a = DynamicObject.deserialize("#M{:a \"first\"}", Mergeable.class); 44 | Mergeable b = DynamicObject.deserialize("#M{:a nil}", Mergeable.class); 45 | 46 | Mergeable c = a.merge(b); 47 | 48 | assertEquals("#M{:a \"first\"}", DynamicObject.serialize(c)); 49 | } 50 | 51 | @Test 52 | public void mergeOutputSerializesCorrectly() { 53 | Mergeable a = DynamicObject.deserialize("#M{:a \"outer\"}", Mergeable.class); 54 | Mergeable b = DynamicObject.deserialize("#M{:m #M{:a \"inner\"}}", Mergeable.class); 55 | 56 | Mergeable c = a.merge(b); 57 | 58 | assertEquivalent("#M{:m #M{:a \"inner\"}, :a \"outer\"}", DynamicObject.serialize(c)); 59 | } 60 | 61 | public interface Mergeable extends DynamicObject { 62 | String a(); 63 | Mergeable m(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/MetadataTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertNull; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | public class MetadataTest { 9 | private static final AnnotatedData AnnotatedData = DynamicObject.deserialize("{:value \"regular data\"}", AnnotatedData.class); 10 | 11 | @Test 12 | public void noInitialMetadata() { 13 | assertNull(AnnotatedData.source()); 14 | assertEquals("{:value \"regular data\"}", DynamicObject.serialize(AnnotatedData)); 15 | } 16 | 17 | @Test 18 | public void metadataBuilders() { 19 | AnnotatedData annotatedData = AnnotatedData.source("SQS"); 20 | assertEquals("SQS", annotatedData.source()); 21 | } 22 | 23 | @Test 24 | public void buildersWithCustomNames() { 25 | AnnotatedData annotatedData = AnnotatedData.withSource("SQS"); 26 | assertEquals("SQS", annotatedData.source()); 27 | } 28 | 29 | @Test 30 | public void customKeys() { 31 | CustomAnnotatedData annotatedData = DynamicObject.newInstance(CustomAnnotatedData.class); 32 | 33 | annotatedData = annotatedData.setSource("Azure"); 34 | 35 | assertEquals("{}", DynamicObject.serialize(annotatedData)); 36 | assertEquals("Azure", annotatedData.getSource()); 37 | } 38 | 39 | @Test 40 | public void metadataIsNotSerialized() { 41 | AnnotatedData annotatedData = AnnotatedData.source("DynamoDB"); 42 | assertEquals("{:value \"regular data\"}", DynamicObject.serialize(annotatedData)); 43 | } 44 | 45 | @Test 46 | public void metadataIsIgnoredForEquality() { 47 | AnnotatedData withMetadata = AnnotatedData.source("Datomic"); 48 | assertEquals(AnnotatedData, withMetadata); 49 | } 50 | 51 | public interface AnnotatedData extends DynamicObject { 52 | @Meta String source(); 53 | AnnotatedData source(String meta); 54 | @Meta @Key(":source") AnnotatedData withSource(String meta); 55 | } 56 | 57 | public interface CustomAnnotatedData extends DynamicObject { 58 | @Meta @Key(":source") String getSource(); 59 | @Meta @Key(":source") CustomAnnotatedData setSource(String source); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/MethodHandleTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import static com.github.rschmitt.dynamicobject.DynamicObject.deserialize; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | 6 | import java.util.UUID; 7 | import java.util.function.BiFunction; 8 | 9 | import org.junit.jupiter.api.Test; 10 | 11 | public class MethodHandleTest { 12 | private static final UUID ReceiptHandle = UUID.randomUUID(); 13 | 14 | @Test 15 | public void buildPolymorphically() { 16 | String edn = "{:command \"start the reactor\"}"; 17 | 18 | QueueMessage queueMessage = deserializeAndAttachMetadata(edn, QueueMessage::receiptHandle, QueueMessage.class); 19 | 20 | assertEquals(ReceiptHandle, queueMessage.receiptHandle()); 21 | assertEquals("start the reactor", queueMessage.command()); 22 | } 23 | 24 | private T deserializeAndAttachMetadata(String edn, BiFunction receiptHandleMetadataBuilder, Class type) { 25 | T instance = deserialize(edn, type); 26 | return receiptHandleMetadataBuilder.apply(instance, ReceiptHandle); 27 | } 28 | 29 | public interface QueueMessage extends DynamicObject { 30 | String command(); 31 | 32 | @Meta UUID receiptHandle(); 33 | QueueMessage receiptHandle(UUID receiptHandle); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/NestingTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import static com.github.rschmitt.dynamicobject.DynamicObject.serialize; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.HashSet; 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.Set; 12 | 13 | import org.junit.jupiter.api.Test; 14 | 15 | public class NestingTest { 16 | @Test 17 | public void nestedInts() { 18 | List innerList = new ArrayList<>(); 19 | innerList.add(1); 20 | innerList.add(2); 21 | innerList.add(3); 22 | List> outerList = new ArrayList<>(); 23 | outerList.add(innerList); 24 | 25 | Nested nested = DynamicObject.newInstance(Nested.class).nestedIntegers(outerList); 26 | 27 | assertEquals(outerList, nested.nestedIntegers()); 28 | assertEquals("{:nestedIntegers [[1 2 3]]}", serialize(nested)); 29 | } 30 | 31 | @Test 32 | public void nestedStrings() { 33 | List innerList = new ArrayList<>(); 34 | innerList.add("str1"); 35 | innerList.add("str2"); 36 | innerList.add("str3"); 37 | List> outerList = new ArrayList<>(); 38 | outerList.add(innerList); 39 | 40 | Nested nested = DynamicObject.newInstance(Nested.class).nestedStrings(outerList); 41 | 42 | assertEquals(outerList, nested.nestedStrings()); 43 | } 44 | 45 | @Test 46 | public void nestedShorts() { 47 | Set innerSet = new HashSet<>(); 48 | innerSet.add((short) 1); 49 | innerSet.add((short) 2); 50 | innerSet.add((short) 3); 51 | List> outerList = new ArrayList<>(); 52 | outerList.add(innerSet); 53 | 54 | Nested nested = DynamicObject.newInstance(Nested.class).nestedShorts(outerList); 55 | 56 | assertEquals(outerList, nested.nestedShorts()); 57 | assertEquals("{:nestedShorts [#{1 3 2}]}", serialize(nested)); 58 | } 59 | 60 | @Test 61 | public void nestedMaps() { 62 | Map innerMap = new HashMap<>(); 63 | innerMap.put(1, 2); 64 | Map> outerMap = new HashMap<>(); 65 | outerMap.put(1, innerMap); 66 | 67 | Nested nested = DynamicObject.newInstance(Nested.class).nestedMaps(outerMap); 68 | 69 | assertEquals(outerMap, nested.nestedMaps()); 70 | assertEquals("{:nestedMaps {1 {1 2}}}", serialize(nested)); 71 | } 72 | 73 | public interface Nested extends DynamicObject { 74 | List> nestedStrings(); 75 | List> nestedIntegers(); 76 | List> nestedShorts(); 77 | Map> nestedMaps(); 78 | 79 | Nested nestedStrings(List> strings); 80 | Nested nestedIntegers(List> integers); 81 | Nested nestedShorts(List> nestedShorts); 82 | Nested nestedMaps(Map> nestedMaps); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/NumberTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import static com.github.rschmitt.dynamicobject.DynamicObject.deserialize; 4 | import static com.github.rschmitt.dynamicobject.DynamicObject.fromFressianByteArray; 5 | import static com.github.rschmitt.dynamicobject.DynamicObject.newInstance; 6 | import static com.github.rschmitt.dynamicobject.DynamicObject.serialize; 7 | import static com.github.rschmitt.dynamicobject.DynamicObject.toFressianByteArray; 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | import java.math.BigDecimal; 11 | import java.math.BigInteger; 12 | 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.Test; 15 | 16 | public class NumberTest { 17 | @BeforeEach 18 | public void setup() { 19 | DynamicObject.registerTag(ArbitraryPrecision.class, "ap"); 20 | } 21 | 22 | @Test 23 | public void BigDecimal() { 24 | String edn = "#ap{:bigDecimal 3.14159M}"; 25 | 26 | ArbitraryPrecision arbitraryPrecision = deserialize(edn, ArbitraryPrecision.class); 27 | 28 | assertEquals(edn, serialize(arbitraryPrecision)); 29 | assertEquals(newInstance(ArbitraryPrecision.class).bigDecimal(new BigDecimal("3.14159")), arbitraryPrecision); 30 | binaryRoundTrip(arbitraryPrecision); 31 | } 32 | 33 | @Test 34 | public void BigInteger() { 35 | String edn = "#ap{:bigInteger 9234812039419082756912384500123N}"; 36 | 37 | ArbitraryPrecision arbitraryPrecision = deserialize(edn, ArbitraryPrecision.class); 38 | 39 | assertEquals(edn, serialize(arbitraryPrecision)); 40 | assertEquals(newInstance(ArbitraryPrecision.class).bigInteger(new BigInteger("9234812039419082756912384500123")), arbitraryPrecision); 41 | binaryRoundTrip(arbitraryPrecision); 42 | } 43 | 44 | private void binaryRoundTrip(Object expected) { 45 | Object actual = fromFressianByteArray(toFressianByteArray(expected)); 46 | assertEquals(expected, actual); 47 | } 48 | 49 | public interface ArbitraryPrecision extends DynamicObject { 50 | BigDecimal bigDecimal(); 51 | BigInteger bigInteger(); 52 | 53 | ArbitraryPrecision bigDecimal(BigDecimal bigDecimal); 54 | ArbitraryPrecision bigInteger(BigInteger bigInteger); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/ObjectMethodsTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.assertFalse; 6 | 7 | @SuppressWarnings("unchecked") 8 | public class ObjectMethodsTest { 9 | @Test 10 | public void equalsNullTest() throws Exception { 11 | assertFalse(DynamicObject.newInstance(DynamicObject.class).equals(null)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/OptionalTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import static com.github.rschmitt.dynamicobject.DynamicObject.deserialize; 4 | import static com.github.rschmitt.dynamicobject.DynamicObject.newInstance; 5 | import static com.github.rschmitt.dynamicobject.DynamicObject.serialize; 6 | import static java.util.Arrays.asList; 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | import static org.junit.jupiter.api.Assertions.assertFalse; 9 | import static org.junit.jupiter.api.Assertions.assertThrows; 10 | 11 | import java.time.Instant; 12 | import java.util.List; 13 | import java.util.Optional; 14 | 15 | import org.junit.jupiter.api.Test; 16 | 17 | public class OptionalTest { 18 | @Test 19 | public void valuePresent() { 20 | OptWrapper instance = deserialize("{:str \"value\"}", OptWrapper.class).validate(); 21 | OptWrapper expected = newInstance(OptWrapper.class).str(Optional.of("value")).validate(); 22 | 23 | assertEquals("value", instance.str().get()); 24 | assertEquals(expected, instance); 25 | } 26 | 27 | @Test 28 | public void valueMissing() { 29 | OptWrapper instance = deserialize("{:str nil}", OptWrapper.class).validate(); 30 | OptWrapper expected = newInstance(OptWrapper.class).str(Optional.empty()).validate(); 31 | 32 | assertFalse(instance.str().isPresent()); 33 | assertEquals(expected, instance); 34 | } 35 | 36 | @Test 37 | public void nonOptionalBuilder() { 38 | OptWrapper nothing = newInstance(OptWrapper.class).rawStr(null).validate(); 39 | OptWrapper some = newInstance(OptWrapper.class).rawStr("some string").validate(); 40 | 41 | assertEquals(some.rawStr(), Optional.of("some string")); 42 | assertEquals(nothing.rawStr(), Optional.empty()); 43 | } 44 | 45 | @Test 46 | public void intPresent() { 47 | OptWrapper instance = deserialize("{:i 24601}", OptWrapper.class).validate(); 48 | OptWrapper expected = newInstance(OptWrapper.class).i(Optional.of(24601)).validate(); 49 | 50 | assertEquals(Integer.valueOf(24601), instance.i().get()); 51 | assertEquals(expected, instance); 52 | } 53 | 54 | @Test 55 | public void listPresent() { 56 | OptWrapper instance = deserialize("{:ints [1 2 3]}", OptWrapper.class).validate(); 57 | OptWrapper expected = newInstance(OptWrapper.class).ints(Optional.of(asList(1, 2, 3))).validate(); 58 | 59 | assertEquals(asList(1, 2, 3), instance.ints().get()); 60 | assertEquals(expected, instance); 61 | } 62 | 63 | @Test 64 | public void instantPresent() { 65 | String edn = "{:inst #inst \"1985-04-12T23:20:50.520-00:00\"}"; 66 | Instant expected = Instant.parse("1985-04-12T23:20:50.52Z"); 67 | 68 | OptWrapper instance = deserialize(edn, OptWrapper.class).validate(); 69 | 70 | assertEquals(expected, instance.inst().get()); 71 | assertEquals(edn, serialize(instance)); 72 | } 73 | 74 | @Test 75 | public void dynamicObjectPresent() { 76 | DynamicObject.registerTag(OptWrapper.class, "OptWrapper"); 77 | 78 | OptWrapper instance = deserialize("#OptWrapper{:wrapper #OptWrapper{:i 24}}", OptWrapper.class).validate(); 79 | OptWrapper expected = newInstance(OptWrapper.class).wrapper(Optional.of(newInstance(OptWrapper.class).i(Optional.of(24)))).validate(); 80 | 81 | assertEquals(expected.wrapper().get(), instance.wrapper().get()); 82 | assertEquals(expected, instance); 83 | 84 | DynamicObject.deregisterTag(OptWrapper.class); 85 | } 86 | 87 | @Test 88 | public void optionalValidation() { 89 | deserialize("{}", OptWrapper.class).validate(); 90 | deserialize("{:str \"value\"}", OptWrapper.class).validate(); 91 | 92 | } 93 | 94 | @Test 95 | public void optionalValidationFailure() { 96 | assertThrows(IllegalStateException.class, () -> deserialize("{:str 4}", OptWrapper.class).validate()); 97 | } 98 | 99 | public interface OptWrapper extends DynamicObject { 100 | Optional str(); 101 | Optional i(); 102 | Optional> ints(); 103 | Optional inst(); 104 | Optional wrapper(); 105 | 106 | OptWrapper str(Optional str); 107 | OptWrapper i(Optional i); 108 | OptWrapper ints(Optional> ints); 109 | OptWrapper inst(Optional inst); 110 | OptWrapper wrapper(Optional wrapper); 111 | 112 | Optional rawStr(); 113 | OptWrapper rawStr(String str); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/PrimitiveTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import static com.github.rschmitt.dynamicobject.DynamicObject.deserialize; 4 | import static com.github.rschmitt.dynamicobject.DynamicObject.serialize; 5 | import static com.github.rschmitt.dynamicobject.TestUtils.assertEquivalent; 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | import static org.junit.jupiter.api.Assertions.assertNull; 8 | import static org.junit.jupiter.api.Assertions.assertTrue; 9 | 10 | import org.junit.jupiter.api.Test; 11 | 12 | public class PrimitiveTest { 13 | private static final String Edn = "{:i 4, :d 3.14, :f 3.14, :lng 1234567890, :shrt 4, :bool true, :c \\newline, :b 127}"; 14 | private static final Unboxed Unboxed = deserialize(Edn, Unboxed.class); 15 | private static final Boxed Boxed = deserialize(Edn, Boxed.class); 16 | 17 | @Test 18 | public void getBoxedFields() { 19 | assertTrue(4 == Boxed.shrt()); 20 | assertTrue(4 == Boxed.i()); 21 | assertTrue(1234567890L == Boxed.lng()); 22 | assertEquals(3.14, Boxed.d(), 0.001); 23 | assertEquals(3.14, Boxed.f(), 0.001); 24 | assertTrue(Boxed.bool()); 25 | assertTrue('\n' == Boxed.c()); 26 | assertTrue((byte) 127 == Boxed.b()); 27 | } 28 | 29 | @Test 30 | public void getUnboxedFields() { 31 | assertTrue(4 == Unboxed.shrt()); 32 | assertTrue(4 == Unboxed.i()); 33 | assertTrue(1234567890L == Unboxed.lng()); 34 | assertEquals(3.14, Unboxed.d(), 0.001); 35 | assertEquals(3.14, Unboxed.f(), 0.001); 36 | assertTrue(Unboxed.bool()); 37 | assertTrue('\n' == Unboxed.c()); 38 | assertTrue(127 == Unboxed.b()); 39 | } 40 | 41 | @Test 42 | public void boxedBuilders() { 43 | Boxed boxed = DynamicObject.newInstance(Boxed.class) 44 | .i(4) 45 | .d(3.14) 46 | .f((float) 3.14) 47 | .lng(1234567890L) 48 | .shrt((short) 4) 49 | .bool(true) 50 | .c('\n') 51 | .b((byte) 127); 52 | 53 | assertEquals(Boxed, boxed); 54 | } 55 | 56 | @Test 57 | public void unboxedBuilders() { 58 | Unboxed unboxed = DynamicObject.newInstance(Unboxed.class) 59 | .i(4) 60 | .d(3.14) 61 | .f((float) 3.14) 62 | .lng(1234567890L) 63 | .shrt((short) 4) 64 | .bool(true) 65 | .c('\n') 66 | .b((byte)127); 67 | 68 | assertEquals(Unboxed, unboxed); 69 | } 70 | 71 | @Test 72 | public void unboxedNullBuilders() { 73 | String edn = "{:b nil, :c nil, :bool nil, :shrt nil, :lng nil, :f nil, :d nil, :i nil}"; 74 | Boxed boxed = DynamicObject.newInstance(Boxed.class) 75 | .i(null) 76 | .d(null) 77 | .f(null) 78 | .lng(null) 79 | .shrt(null) 80 | .bool(null) 81 | .c(null) 82 | .b(null); 83 | 84 | assertEquals(boxed, deserialize(edn, Boxed.class)); 85 | assertEquivalent(edn, serialize(boxed)); 86 | assertEquals(deserialize(edn, Boxed.class), boxed); 87 | assertEquivalent(serialize(boxed), edn); 88 | assertNull(boxed.shrt()); 89 | assertNull(boxed.i()); 90 | assertNull(boxed.lng()); 91 | assertNull(boxed.f()); 92 | assertNull(boxed.d()); 93 | assertNull(boxed.bool()); 94 | assertNull(boxed.c()); 95 | assertNull(boxed.b()); 96 | } 97 | 98 | @Test 99 | public void unboxedRoundTrip() { 100 | Unboxed unboxed = deserialize(Edn, Unboxed.class); 101 | assertEquals(Edn, unboxed.toString()); 102 | } 103 | 104 | @Test 105 | public void boxedRoundTrip() { 106 | Boxed boxed = deserialize(Edn, Boxed.class); 107 | assertEquals(Edn, boxed.toString()); 108 | } 109 | 110 | @Test 111 | public void testEquality() { 112 | assertEquals(Boxed, Unboxed); 113 | } 114 | 115 | public interface Unboxed extends DynamicObject { 116 | short shrt(); 117 | int i(); 118 | long lng(); 119 | float f(); 120 | double d(); 121 | boolean bool(); 122 | char c(); 123 | byte b(); 124 | 125 | Unboxed shrt(short shrt); 126 | Unboxed i(int i); 127 | Unboxed lng(long lng); 128 | Unboxed f(float f); 129 | Unboxed d(double d); 130 | Unboxed bool(boolean bool); 131 | Unboxed c(char c); 132 | Unboxed b(byte b); 133 | } 134 | 135 | public interface Boxed extends DynamicObject { 136 | Short shrt(); 137 | Integer i(); 138 | Long lng(); 139 | Float f(); 140 | Double d(); 141 | Boolean bool(); 142 | Character c(); 143 | Byte b(); 144 | 145 | Boxed shrt(Short shrt); 146 | Boxed i(Integer i); 147 | Boxed lng(Long lng); 148 | Boxed f(Float f); 149 | Boxed d(Double d); 150 | Boxed bool(Boolean bool); 151 | Boxed c(Character c); 152 | Boxed b(Byte b); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/PrintingTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import static com.github.rschmitt.dynamicobject.DynamicObject.newInstance; 4 | import static com.github.rschmitt.dynamicobject.DynamicObject.registerTag; 5 | import static com.github.rschmitt.dynamicobject.DynamicObject.serialize; 6 | import static com.github.rschmitt.dynamicobject.TestUtils.assertEquivalent; 7 | 8 | import org.junit.jupiter.api.BeforeAll; 9 | import org.junit.jupiter.api.Test; 10 | 11 | public class PrintingTest { 12 | static Tagged emptyTagged; 13 | static Untagged emptyUntagged; 14 | static Tagged nestedTagged; 15 | static Untagged nestedUntagged; 16 | 17 | @BeforeAll 18 | public static void setup() { 19 | registerTag(Tagged.class, "Tagged"); 20 | emptyTagged = newInstance(Tagged.class); 21 | emptyUntagged = newInstance(Untagged.class); 22 | 23 | nestedTagged = emptyTagged.tagged(emptyTagged).untagged(emptyUntagged); 24 | nestedUntagged = emptyUntagged.tagged(emptyTagged).untagged(emptyUntagged); 25 | } 26 | 27 | @Test 28 | public void toStringTest() { 29 | assertEquivalent("#Tagged{}", emptyTagged.toString()); 30 | assertEquivalent("{}", emptyUntagged.toString()); 31 | assertEquivalent("#Tagged{:untagged {}, :tagged #Tagged{}}", nestedTagged.toString()); 32 | assertEquivalent("{:untagged {}, :tagged #Tagged{}}", nestedUntagged.toString()); 33 | } 34 | 35 | @Test 36 | public void toFormattedStringTest() { 37 | assertEquivalent("#Tagged{}", emptyTagged.toFormattedString()); 38 | assertEquivalent("{}", emptyUntagged.toFormattedString()); 39 | assertEquivalent("#Tagged{:untagged {}, :tagged #Tagged{}}", nestedTagged.toFormattedString()); 40 | assertEquivalent("{:untagged {}, :tagged #Tagged{}}", nestedUntagged.toFormattedString()); 41 | } 42 | 43 | @Test 44 | public void serializeTest() { 45 | assertEquivalent("#Tagged{}", serialize(emptyTagged)); 46 | assertEquivalent("{}", serialize(emptyUntagged)); 47 | assertEquivalent("#Tagged{:untagged {}, :tagged #Tagged{}}", serialize(nestedTagged)); 48 | assertEquivalent("{:untagged {}, :tagged #Tagged{}}", serialize(nestedUntagged)); 49 | } 50 | 51 | public interface Tagged extends DynamicObject { 52 | @Key(":tagged") Tagged tagged(Tagged tagged); 53 | @Key(":untagged") Tagged untagged(Untagged untagged); 54 | } 55 | 56 | public interface Untagged extends DynamicObject { 57 | @Key(":tagged") Untagged tagged(Tagged tagged); 58 | @Key(":untagged") Untagged untagged(Untagged untagged); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/RecordTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import static java.lang.String.format; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | import static org.junit.jupiter.api.Assertions.assertThrows; 6 | 7 | import org.junit.jupiter.api.AfterEach; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | 11 | public class RecordTest { 12 | @BeforeEach 13 | public void setup() { 14 | DynamicObject.registerTag(Defrecord.class, "com.github.rschmitt.dynamicobject.Defrecord"); 15 | DynamicObject.registerTag(Random.class, "com.github.rschmitt.dynamicobject.Random"); 16 | } 17 | 18 | @AfterEach 19 | public void teardown() { 20 | DynamicObject.deregisterTag(Defrecord.class); 21 | DynamicObject.deregisterTag(Random.class); 22 | } 23 | 24 | @Test 25 | public void roundTrip() { 26 | String edn = "#com.github.rschmitt.dynamicobject.Defrecord{:str \"a string\"}"; 27 | 28 | Defrecord record = DynamicObject.deserialize(edn, Defrecord.class); 29 | 30 | assertEquals("a string", record.str()); 31 | assertEquals(edn, DynamicObject.serialize(record)); 32 | } 33 | 34 | @Test 35 | public void pprintIncludesReaderTag() { 36 | String edn = "#com.github.rschmitt.dynamicobject.Defrecord{:str \"a string\"}"; 37 | Defrecord record = DynamicObject.deserialize(edn, Defrecord.class); 38 | assertEquals(format("%s%n", edn), record.toFormattedString()); 39 | } 40 | 41 | @Test 42 | public void suppliedTypeMustMatchReaderTag() { 43 | String edn = "#com.github.rschmitt.dynamicobject.Defrecord{:str \"a string\"}"; 44 | 45 | assertThrows(ClassCastException.class, () -> DynamicObject.deserialize(edn, Random.class)); 46 | } 47 | 48 | public interface Defrecord extends DynamicObject { 49 | String str(); 50 | } 51 | 52 | public interface Random extends DynamicObject { 53 | String str(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/RecursionTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | 4 | import static com.github.rschmitt.dynamicobject.DynamicObject.deserialize; 5 | import static com.github.rschmitt.dynamicobject.DynamicObject.fromFressianByteArray; 6 | import static com.github.rschmitt.dynamicobject.DynamicObject.newInstance; 7 | import static com.github.rschmitt.dynamicobject.DynamicObject.serialize; 8 | import static com.github.rschmitt.dynamicobject.DynamicObject.toFressianByteArray; 9 | import static com.github.rschmitt.dynamicobject.TestUtils.assertEquivalent; 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | import static org.junit.jupiter.api.Assertions.assertNull; 12 | import static org.junit.jupiter.api.Assertions.assertTrue; 13 | 14 | import org.junit.jupiter.api.BeforeEach; 15 | import org.junit.jupiter.api.Test; 16 | 17 | public class RecursionTest { 18 | @BeforeEach 19 | public void setup() { 20 | try { 21 | DynamicObject.deregisterTag(LinkedList.class); 22 | } catch (NullPointerException ignore) { } 23 | } 24 | 25 | @Test 26 | public void recursion() { 27 | LinkedList tail = newInstance(LinkedList.class).value(3); 28 | LinkedList middle = newInstance(LinkedList.class).value(2).next(tail); 29 | LinkedList head = newInstance(LinkedList.class).value(1).next(middle); 30 | 31 | roundTrip(tail, false); 32 | roundTrip(middle, false); 33 | roundTrip(head, false); 34 | 35 | assertEquals(1, head.value()); 36 | assertEquals(2, head.next().value()); 37 | assertEquals(3, head.next().next().value()); 38 | assertNull(head.next().next().next()); 39 | 40 | assertEquivalent("{:next {:next {:value 3}, :value 2}, :value 1}", serialize(head)); 41 | assertEquivalent("{:next {:value 3}, :value 2}", serialize(middle)); 42 | assertEquivalent("{:value 3}", serialize(tail)); 43 | } 44 | 45 | @Test 46 | public void taggedRecursion() { 47 | DynamicObject.registerTag(LinkedList.class, "LinkedList"); 48 | 49 | LinkedList tail = newInstance(LinkedList.class).value(3); 50 | LinkedList middle = newInstance(LinkedList.class).value(2).next(tail); 51 | LinkedList head = newInstance(LinkedList.class).value(1).next(middle); 52 | 53 | roundTrip(tail, true); 54 | roundTrip(middle, true); 55 | roundTrip(head, true); 56 | assertEquivalent("#LinkedList{:value 3}", serialize(tail)); 57 | assertEquivalent("#LinkedList{:next #LinkedList{:value 3}, :value 2}", serialize(middle)); 58 | assertEquivalent("#LinkedList{:next #LinkedList{:next #LinkedList{:value 3}, :value 2}, :value 1}", serialize(head)); 59 | } 60 | 61 | @Test 62 | public void registeringTheTagAddsItToSerializedOutput() { 63 | LinkedList tail = newInstance(LinkedList.class).value(3); 64 | LinkedList middle = newInstance(LinkedList.class).value(2).next(tail); 65 | LinkedList head = newInstance(LinkedList.class).value(1).next(middle); 66 | 67 | DynamicObject.registerTag(LinkedList.class, "LinkedList"); 68 | 69 | roundTrip(tail, true); 70 | roundTrip(middle, true); 71 | roundTrip(head, true); 72 | assertEquivalent("#LinkedList{:value 3}", serialize(tail)); 73 | assertEquivalent("#LinkedList{:next #LinkedList{:value 3}, :value 2}", serialize(middle)); 74 | assertEquivalent("#LinkedList{:next #LinkedList{:next #LinkedList{:value 3}, :value 2}, :value 1}", serialize(head)); 75 | } 76 | 77 | @Test 78 | public void deregisteringTheTagRemovesItFromSerializedOutput() { 79 | DynamicObject.registerTag(LinkedList.class, "LinkedList"); 80 | 81 | LinkedList tail = newInstance(LinkedList.class).value(3); 82 | LinkedList middle = newInstance(LinkedList.class).value(2).next(tail); 83 | LinkedList head = newInstance(LinkedList.class).value(1).next(middle); 84 | 85 | DynamicObject.deregisterTag(LinkedList.class); 86 | 87 | roundTrip(tail, false); 88 | roundTrip(middle, false); 89 | roundTrip(head, false); 90 | 91 | assertEquivalent("{:next {:next {:value 3}, :value 2}, :value 1}", serialize(head)); 92 | assertEquivalent("{:next {:value 3}, :value 2}", serialize(middle)); 93 | assertEquivalent("{:value 3}", serialize(tail)); 94 | } 95 | 96 | @Test 97 | public void registeringTheTagDoesNotAffectEqualityOfDeserializedInstances() { 98 | LinkedList obj1 = DynamicObject.deserialize("{:value 1, :next {:value 2, :next {:value 3}}}", LinkedList.class); 99 | DynamicObject.registerTag(LinkedList.class, "LinkedList"); 100 | LinkedList obj2 = DynamicObject.deserialize("#LinkedList{:value 1, :next #LinkedList{:value 2, :next #LinkedList{:value 3}}}", LinkedList.class); 101 | DynamicObject.deregisterTag(LinkedList.class); 102 | 103 | LinkedList next = obj1.next().next(); 104 | LinkedList next2 = obj1.next().next(); 105 | assertEquals(next, next2); 106 | assertTrue(next.equals(next2)); 107 | assertTrue(obj1.equals(obj2)); 108 | assertEquals(obj1.next(), obj2.next()); 109 | assertEquals(obj1, obj2); 110 | assertEquals(DynamicObject.serialize(obj1), DynamicObject.serialize(obj2)); 111 | } 112 | 113 | private void roundTrip(LinkedList linkedList, boolean binary) { 114 | assertEquals(linkedList, deserialize(serialize(linkedList), LinkedList.class)); 115 | if (binary) 116 | assertEquals(linkedList, fromFressianByteArray(toFressianByteArray(linkedList))); 117 | } 118 | 119 | public interface LinkedList extends DynamicObject { 120 | long value(); 121 | LinkedList next(); 122 | 123 | LinkedList value(long value); 124 | LinkedList next(LinkedList linkedList); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/SchemaCollectionTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import static com.github.rschmitt.dynamicobject.DynamicObject.deserialize; 4 | import static com.github.rschmitt.dynamicobject.DynamicObject.newInstance; 5 | import static com.github.rschmitt.dynamicobject.DynamicObject.serialize; 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | 8 | import java.util.ArrayList; 9 | import java.util.HashMap; 10 | import java.util.HashSet; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.Set; 14 | 15 | import org.junit.jupiter.api.AfterEach; 16 | import org.junit.jupiter.api.BeforeEach; 17 | import org.junit.jupiter.api.Test; 18 | 19 | public class SchemaCollectionTest { 20 | @BeforeEach 21 | public void setup() { 22 | DynamicObject.registerTag(X.class, "X"); 23 | DynamicObject.registerTag(Coll.class, "Coll"); 24 | } 25 | 26 | @AfterEach 27 | public void teardown() { 28 | DynamicObject.deregisterTag(Coll.class); 29 | DynamicObject.deregisterTag(X.class); 30 | } 31 | 32 | @Test 33 | public void list() { 34 | Coll expected = deserialize("#Coll{:list [#X{:y 1}, #X{:y 2}, #X{:y 3}]}", Coll.class); 35 | List list = new ArrayList<>(); 36 | list.add(newInstance(X.class).y(1)); 37 | list.add(newInstance(X.class).y(2)); 38 | list.add(newInstance(X.class).y(3)); 39 | 40 | Coll actual = newInstance(Coll.class).list(list); 41 | 42 | roundTripTest(expected, Coll.class); 43 | roundTripTest(actual, Coll.class); 44 | assertEquals(expected, actual); 45 | assertEquals(list, expected.list()); 46 | assertEquals(list, actual.list()); 47 | assertEquals(actual.list(), list); 48 | assertEquals(expected.list(), list); 49 | } 50 | 51 | @Test 52 | public void set() { 53 | Coll expected = deserialize("#Coll{:set #{#X{:y 1} #X{:y 2} #X{:y 3}}}", Coll.class); 54 | Set set = new HashSet<>(); 55 | set.add(newInstance(X.class).y(1)); 56 | set.add(newInstance(X.class).y(3)); 57 | set.add(newInstance(X.class).y(2)); 58 | 59 | Coll actual = newInstance(Coll.class).set(set); 60 | 61 | roundTripTest(expected, Coll.class); 62 | roundTripTest(actual, Coll.class); 63 | assertEquals(expected, actual); 64 | assertEquals(set, expected.set()); 65 | assertEquals(set, actual.set()); 66 | assertEquals(actual.set(), set); 67 | assertEquals(expected.set(), set); 68 | } 69 | 70 | @Test 71 | public void map() { 72 | Coll expected = deserialize("#Coll{:map {#X{:y 1}, #X{:y 2}}}", Coll.class); 73 | Map map = new HashMap<>(); 74 | map.put(newInstance(X.class).y(1), newInstance(X.class).y(2)); 75 | 76 | Coll actual = newInstance(Coll.class).map(map); 77 | 78 | roundTripTest(expected, Coll.class); 79 | roundTripTest(actual, Coll.class); 80 | assertEquals(expected, actual); 81 | assertEquals(map, expected.map()); 82 | assertEquals(map, actual.map()); 83 | assertEquals(actual.map(), map); 84 | assertEquals(expected.map(), map); 85 | } 86 | 87 | private > void roundTripTest(D obj, Class type) { 88 | String edn = serialize(obj); 89 | D actual = deserialize(edn, type); 90 | assertEquals(obj, actual); 91 | } 92 | 93 | public interface Coll extends DynamicObject { 94 | List list(); 95 | Set set(); 96 | Map map(); 97 | 98 | Coll list(List list); 99 | Coll set(Set set); 100 | Coll map(Map map); 101 | } 102 | 103 | public interface X extends DynamicObject { 104 | int y(); 105 | 106 | X y(int y); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/StaticFieldTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertNull; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | /* 8 | * This test exposes a bug where proxyCache#computeIfAbsent in Instances was being called 9 | * recursively, resulting in a non-terminating program (JDK-8062841) on some JDK versions. The call 10 | * to 'newInstance' causes DO to try to initialize the DynamicProxy for that type, which causes the 11 | * static initializer to run, which results in 'newInstance' being called to initialize the static 12 | * field, which results in a recursive call to proxyCache#computeIfAbsent, which results in 13 | * undefined behavior. The fix is simply to force class loading before attempting to create a 14 | * DynamicProxy. 15 | */ 16 | public class StaticFieldTest { 17 | @Test 18 | public void asdf() throws Exception { 19 | Holder holder = DynamicObject.newInstance(Holder.class); 20 | assertNull(holder.getMap().get("asdf")); 21 | } 22 | 23 | public interface Holder extends DynamicObject { 24 | Holder holder = DynamicObject.newInstance(Holder.class); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/StreamingTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import clojure.java.api.Clojure; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.io.ByteArrayInputStream; 7 | import java.io.IOException; 8 | import java.io.PushbackReader; 9 | import java.io.StringReader; 10 | import java.util.Iterator; 11 | import java.util.List; 12 | import java.util.stream.Stream; 13 | 14 | import static com.github.rschmitt.dynamicobject.DynamicObject.deserializeStream; 15 | import static java.util.Arrays.asList; 16 | import static java.util.stream.Collectors.toList; 17 | import static org.junit.jupiter.api.Assertions.*; 18 | 19 | @SuppressWarnings("rawtypes") 20 | public class StreamingTest { 21 | @Test 22 | public void iteratorTest() { 23 | String edn = "{:x 1} {:x 2}"; 24 | PushbackReader reader = new PushbackReader(new StringReader(edn)); 25 | 26 | Iterator iterator = deserializeStream(reader, StreamingType.class).iterator(); 27 | 28 | assertTrue(iterator.hasNext()); 29 | assertTrue(iterator.hasNext()); 30 | assertEquals(1, iterator.next().x()); 31 | assertTrue(iterator.hasNext()); 32 | assertTrue(iterator.hasNext()); 33 | assertEquals(2, iterator.next().x()); 34 | assertFalse(iterator.hasNext()); 35 | assertFalse(iterator.hasNext()); 36 | } 37 | 38 | @Test 39 | public void streamTest() { 40 | String edn = "{:x 1}{:x 2}{:x 3}"; 41 | PushbackReader reader = new PushbackReader(new StringReader(edn)); 42 | 43 | Stream stream = deserializeStream(reader, StreamingType.class); 44 | 45 | assertEquals(6, stream.mapToInt(StreamingType::x).sum()); 46 | } 47 | 48 | @Test 49 | public void plainStreamTest() { 50 | String edn = "\"string one\"\n\"string two\""; 51 | PushbackReader reader = new PushbackReader(new StringReader(edn)); 52 | 53 | List list = deserializeStream(reader, String.class).collect(toList()); 54 | 55 | assertEquals(asList("string one", "string two"), list); 56 | } 57 | 58 | @Test 59 | public void heterogeneousStreamTest() { 60 | List expected = asList(42L, "string", Clojure.read("{:key :value}"), asList('a', 'b', 'c')); 61 | String edn = "42 \"string\" {:key :value} [\\a \\b \\c]"; 62 | PushbackReader reader = new PushbackReader(new StringReader(edn)); 63 | 64 | List actual = deserializeStream(reader, Object.class).collect(toList()); 65 | 66 | assertEquals(expected, actual); 67 | } 68 | 69 | @Test 70 | public void nilTest() { 71 | assertThrows(NullPointerException.class, () -> 72 | deserializeStream(new PushbackReader(new StringReader("nil")), StreamingType.class) 73 | .collect(toList())); 74 | } 75 | 76 | @Test 77 | public void emptyFressianStreamTest() throws IOException { 78 | List list = DynamicObject.deserializeFressianStream( 79 | new ByteArrayInputStream(new byte[]{ }), 80 | Object.class 81 | ).collect(toList()); 82 | assertTrue(list.isEmpty()); 83 | } 84 | 85 | public interface StreamingType extends DynamicObject { 86 | int x(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/TestUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject; 2 | 3 | import static com.github.rschmitt.dynamicobject.internal.ClojureStuff.Assoc; 4 | import static com.github.rschmitt.dynamicobject.internal.ClojureStuff.Default; 5 | import static com.github.rschmitt.dynamicobject.internal.ClojureStuff.EmptyMap; 6 | import static com.github.rschmitt.dynamicobject.internal.ClojureStuff.ReadString; 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | import clojure.lang.AFn; 10 | 11 | public class TestUtils { 12 | private static final Object readerOpts = Assoc.invoke(EmptyMap, Default, getUnknownReader()); 13 | 14 | private static Object getUnknownReader() { 15 | return new AFn() { 16 | @Override 17 | public Object invoke(Object arg1, Object arg2) { 18 | return new Unknown(arg1.toString(), arg2); 19 | } 20 | }; 21 | } 22 | 23 | public static Object genericRead(String str) { 24 | return ReadString.invoke(readerOpts, str); 25 | } 26 | 27 | public static void assertEquivalent(String expected, String actual) { 28 | assertEquals(genericRead(expected), genericRead(actual)); 29 | } 30 | 31 | public static void assertEquivalent(String message, String expected, String actual) { 32 | assertEquals(genericRead(expected), genericRead(actual), message); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/benchmark/DeserializationBenchmark.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject.benchmark; 2 | 3 | import com.github.rschmitt.dynamicobject.DynamicObject; 4 | import com.github.rschmitt.dynamicobject.Key; 5 | import org.fressian.FressianWriter; 6 | import org.junit.jupiter.api.Tag; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.io.ByteArrayInputStream; 10 | import java.io.ByteArrayOutputStream; 11 | import java.io.IOException; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import java.util.concurrent.ExecutorService; 15 | import java.util.concurrent.Executors; 16 | import java.util.concurrent.Future; 17 | 18 | public class DeserializationBenchmark { 19 | private final int ITERATIONS = 1_000_000; 20 | private final int NUM_THREADS = 8; 21 | 22 | @Test 23 | @Tag("benchmark") 24 | public void run() throws Exception { 25 | DynamicObject.registerTag(StringFieldList.class, "string-field-list-tag"); 26 | DynamicObject.registerTag(StringField.class, "string-field-tag"); 27 | ExecutorService executorService = Executors.newCachedThreadPool(); 28 | List> futures = new ArrayList<>(); 29 | 30 | long acc = 0; 31 | long startTime = System.nanoTime(); 32 | for (int i = 0; i < NUM_THREADS; i++) { 33 | futures.add(executorService.submit(this::perfTest)); 34 | } 35 | 36 | for (Future future : futures) { 37 | acc += future.get(); 38 | } 39 | long endTime = System.nanoTime(); 40 | 41 | System.out.println("Total bytes deserialized (MiB):" + acc / 1024.0 / 1024.0); 42 | reportTime("stringField", startTime, endTime); 43 | } 44 | 45 | private long perfTest() { 46 | StringFieldList stringFieldList = getStringFieldList(); 47 | byte[] buffer = serialize(stringFieldList); 48 | 49 | System.out.println("Serialization size (B) = " + buffer.length); 50 | long bytesDeserialized = 0; 51 | for (int i = 0; i < ITERATIONS; i++) { 52 | deserialize(buffer); 53 | bytesDeserialized += buffer.length; 54 | } 55 | return bytesDeserialized; 56 | } 57 | 58 | private void reportTime(String desc, long startTime, long endTime) { 59 | long timeInMillis = (endTime - startTime) / 1000000; 60 | System.out.println(String.format("%s: %,d ms", desc, timeInMillis)); 61 | } 62 | 63 | private StringFieldList deserialize(byte[] buffer) { 64 | try { 65 | return (StringFieldList) DynamicObject.createFressianReader(new ByteArrayInputStream(buffer), false).readObject(); 66 | } catch (IOException e) { 67 | throw new RuntimeException(e); 68 | } 69 | } 70 | 71 | private byte[] serialize(StringFieldList stringFieldList) { 72 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 73 | FressianWriter writer = DynamicObject.createFressianWriter(baos); 74 | try { 75 | writer.writeObject(stringFieldList); 76 | } catch (IOException e) { 77 | throw new RuntimeException(e); 78 | } 79 | return baos.toByteArray(); 80 | } 81 | 82 | private StringFieldList getStringFieldList() { 83 | List stringFields = new ArrayList<>(); 84 | for (int i = 0; i < 100; i++) { 85 | stringFields.add(DynamicObject.newInstance(StringField.class).withString("str")); 86 | } 87 | return DynamicObject.newInstance(StringFieldList.class) 88 | .withStringFields(stringFields); 89 | } 90 | 91 | public interface StringFieldList extends DynamicObject { 92 | @Key(":string-fields") StringField getStringFields(); 93 | @Key(":string-fields") StringFieldList withStringFields(List stringList); 94 | } 95 | 96 | public interface StringField extends DynamicObject { 97 | @Key(":string-field") String getString(); 98 | @Key(":string-field") StringField withString(String string); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/benchmark/PrimitiveFieldAccess.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject.benchmark; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import com.github.rschmitt.dynamicobject.DynamicObject; 6 | 7 | public class PrimitiveFieldAccess { 8 | private final int Iterations = 500_000; 9 | 10 | @Test 11 | public void run() { 12 | IntPojo intPojo = new IntPojo(1); 13 | FinalIntPojo finalIntPojo = new FinalIntPojo(1); 14 | IntField intField = DynamicObject.newInstance(IntField.class).i(1); 15 | 16 | int acc = 0; 17 | for (int i = 0; i < 15_000; i++) { 18 | acc += intPojo.getI(); 19 | acc += finalIntPojo.getI(); 20 | acc += intField.i(); 21 | } 22 | System.out.println(acc); 23 | 24 | acc = 0; 25 | long startTime = System.nanoTime(); 26 | for (int i = 0; i < Iterations; i++) { 27 | acc += intPojo.getI(); 28 | } 29 | long endTime = System.nanoTime(); 30 | System.out.println(acc); 31 | reportTime("intPojo", startTime, endTime); 32 | 33 | 34 | acc = 0; 35 | startTime = System.nanoTime(); 36 | for (int i = 0; i < Iterations; i++) { 37 | acc += finalIntPojo.getI(); 38 | } 39 | endTime = System.nanoTime(); 40 | System.out.println(acc); 41 | reportTime("finalIntPojo", startTime, endTime); 42 | 43 | 44 | acc = 0; 45 | startTime = System.nanoTime(); 46 | for (int i = 0; i < Iterations; i++) { 47 | acc += intField.i(); 48 | } 49 | endTime = System.nanoTime(); 50 | System.out.println(acc); 51 | reportTime("intField", startTime, endTime); 52 | } 53 | 54 | private void reportTime(String desc, long startTime, long endTime) { 55 | long timeInMillis = (endTime - startTime) / 1000000; 56 | System.out.println(String.format("%s: %,d ms", desc, timeInMillis)); 57 | } 58 | 59 | public interface IntField extends DynamicObject { 60 | int i(); 61 | 62 | IntField i(int i); 63 | } 64 | } 65 | 66 | class IntPojo { 67 | int i; 68 | 69 | IntPojo(int i) { 70 | this.i = i; 71 | } 72 | 73 | int getI() { 74 | return i; 75 | } 76 | } 77 | 78 | class FinalIntPojo { 79 | private final int i; 80 | 81 | FinalIntPojo(int i) { 82 | this.i = i; 83 | } 84 | 85 | int getI() { 86 | return i; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/benchmark/StringFieldAccess.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject.benchmark; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import com.github.rschmitt.dynamicobject.DynamicObject; 6 | 7 | public class StringFieldAccess { 8 | private final int Iterations = 500_000; 9 | 10 | @Test 11 | public void run() { 12 | StringPojo stringPojo = new StringPojo("str"); 13 | FinalStringPojo finalStringPojo = new FinalStringPojo("str"); 14 | StringField stringField = DynamicObject.newInstance(StringField.class).str("str"); 15 | 16 | int acc = 0; 17 | for (int i = 0; i < 15_000; i++) { 18 | acc += stringPojo.str().length(); 19 | acc += finalStringPojo.str().length(); 20 | acc += stringField.str().length(); 21 | } 22 | 23 | acc = 0; 24 | long startTime = System.nanoTime(); 25 | for (int i = 0; i < Iterations; i++) { 26 | acc += stringPojo.str().length(); 27 | } 28 | long endTime = System.nanoTime(); 29 | System.out.println(acc); 30 | reportTime("stringPojo", startTime, endTime); 31 | 32 | 33 | acc = 0; 34 | startTime = System.nanoTime(); 35 | for (int i = 0; i < Iterations; i++) { 36 | acc += finalStringPojo.str().length(); 37 | } 38 | endTime = System.nanoTime(); 39 | System.out.println(acc); 40 | reportTime("finalStringPojo", startTime, endTime); 41 | 42 | 43 | acc = 0; 44 | startTime = System.nanoTime(); 45 | for (int i = 0; i < Iterations; i++) { 46 | acc += stringField.str().length(); 47 | } 48 | endTime = System.nanoTime(); 49 | System.out.println(acc); 50 | reportTime("stringField", startTime, endTime); 51 | } 52 | 53 | private void reportTime(String desc, long startTime, long endTime) { 54 | long timeInMillis = (endTime - startTime) / 1000000; 55 | System.out.println(String.format("%s: %,d ms", desc, timeInMillis)); 56 | } 57 | 58 | public interface StringField extends DynamicObject { 59 | String str(); 60 | 61 | StringField str(String str); 62 | } 63 | } 64 | 65 | class StringPojo { 66 | String str; 67 | 68 | StringPojo(String str) { 69 | this.str = str; 70 | } 71 | 72 | String str() { 73 | return str; 74 | } 75 | } 76 | 77 | class FinalStringPojo { 78 | private final String str; 79 | 80 | FinalStringPojo(String str) { 81 | this.str = str; 82 | } 83 | 84 | String str() { 85 | return str; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/test/java/com/github/rschmitt/dynamicobject/internal/ReflectionTest.java: -------------------------------------------------------------------------------- 1 | package com.github.rschmitt.dynamicobject.internal; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import java.lang.reflect.Method; 7 | import java.util.Arrays; 8 | import java.util.Collection; 9 | import java.util.HashSet; 10 | 11 | import org.junit.jupiter.api.Test; 12 | 13 | import com.github.rschmitt.dynamicobject.Cached; 14 | import com.github.rschmitt.dynamicobject.DynamicObject; 15 | import com.github.rschmitt.dynamicobject.Key; 16 | import com.github.rschmitt.dynamicobject.Meta; 17 | import com.github.rschmitt.dynamicobject.Required; 18 | 19 | import clojure.java.api.Clojure; 20 | 21 | public class ReflectionTest { 22 | @Test 23 | public void fieldGetters() throws Exception { 24 | Collection methods = Reflection.fieldGetters(VariousMethods.class); 25 | 26 | assertEquals(2, methods.size()); 27 | assertTrue(methods.contains(VariousMethods.class.getMethod("num"))); 28 | assertTrue(methods.contains(VariousMethods.class.getMethod("customKey"))); 29 | } 30 | 31 | @Test 32 | public void requiredFields() throws Exception { 33 | Collection methods = Reflection.requiredFields(VariousMethods.class); 34 | 35 | assertEquals(1, methods.size()); 36 | assertTrue(methods.contains(VariousMethods.class.getMethod("num"))); 37 | } 38 | 39 | @Test 40 | public void testCachedAnnotations() throws Exception { 41 | HashSet expectedKeys = new HashSet<>(Arrays.asList( 42 | Clojure.read(":cachedGetter"), 43 | Clojure.read(":cachedGetterWithKey"), 44 | Clojure.read(":cachedBuilder"), 45 | Clojure.read(":cachedBuilderWithKey"), 46 | Clojure.read(":nameFromGetter") 47 | )); 48 | 49 | HashSet actualKeys = new HashSet<>(Reflection.cachedKeys(CachedAnnotationTests.class)); 50 | 51 | assertEquals(expectedKeys, actualKeys); 52 | } 53 | 54 | public interface CachedAnnotationTests extends DynamicObject { 55 | @Cached Object cachedGetter(); 56 | @Key(":cachedGetterWithKey") @Cached Object differentMethodName(); 57 | 58 | Object cachedBuilder(); 59 | @Cached CachedAnnotationTests cachedBuilder(Object value); 60 | @Key(":cachedBuilderWithKey") @Cached CachedAnnotationTests differentMethodName2(Object value); 61 | 62 | @Key(":nameFromGetter") Object nameFromGetterFunc(); 63 | @Cached CachedAnnotationTests nameFromGetterFunc(Object value); 64 | } 65 | } 66 | 67 | interface VariousMethods extends DynamicObject { 68 | @Required int num(); 69 | @Key(":custom-key") String customKey(); 70 | @Meta String someMetadata(); 71 | 72 | VariousMethods num(int num); 73 | 74 | default void customMethod() { 75 | 76 | } 77 | 78 | default boolean anotherCustomMethod(int x) { 79 | return true; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test-all-versions.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | 5 | versions=(1.{6,7,8,9,10}.0) 6 | 7 | for i in ${versions[@]} 8 | do 9 | cp build.gradle.kts build-$i.gradle.kts 10 | perl -i -pe 's/\[1.6.0,\)/'"$i"'/g' build-$i.gradle.kts 11 | ./gradlew clean build -b build-$i.gradle.kts 12 | done 13 | 14 | for i in ${versions[@]} 15 | do 16 | rm build-$i.gradle.kts 17 | done 18 | --------------------------------------------------------------------------------