Compare commits
421 commits
Author | SHA1 | Date | |
---|---|---|---|
|
6ffb94647b | ||
|
687acba760 | ||
|
54c44c5570 | ||
|
6a1a15b974 | ||
|
10ac30b0ed | ||
|
0142e2aaf0 | ||
|
f1965f0f46 | ||
|
7e417840e2 | ||
|
0943ef5a61 | ||
|
a5020ddb3c | ||
|
11a547023c | ||
|
bcb6fd4a4d | ||
|
3994723c3c | ||
|
30c973e494 | ||
|
1c3dc89da1 | ||
|
3e848c0cdd | ||
|
be6f3eb2af | ||
|
a83a9b79e4 | ||
|
d843950731 | ||
|
13a40edf5b | ||
|
126c8ea961 | ||
|
f68035859e | ||
|
e04c80c1c6 | ||
|
b8610c7fc7 | ||
|
136df54467 | ||
|
ded3bccc60 | ||
|
3588c98b86 | ||
|
fead71bca0 | ||
|
5153295988 | ||
|
e6de367369 | ||
|
9f6197b67c | ||
|
6393692926 | ||
|
85e86f1fa8 | ||
|
2e241bff6f | ||
|
ca4d8f5e34 | ||
|
a2b39e29e8 | ||
|
737f58cecb | ||
|
9aaef1f6f8 | ||
|
2265f5e9b8 | ||
|
22259af1d1 | ||
|
1f2a9373b7 | ||
|
dbe7bfa15f | ||
|
afb82cb3e8 | ||
|
c64138bf0a | ||
|
4550da0abe | ||
|
f4f7c656bd | ||
|
7d1657d76c | ||
|
a920b9be3a | ||
|
1f46203285 | ||
|
8b17ecedae | ||
|
5f0b2ee8e3 | ||
|
6e6abf54ab | ||
|
38b08f81e6 | ||
|
5bf14098d1 | ||
|
5f5631e34f | ||
|
98459dd8ab | ||
|
6b6c1af907 | ||
|
eb7110bf59 | ||
|
69d4f40a12 | ||
|
878b3ff93b | ||
|
103ae3cec6 | ||
|
81db25825d | ||
|
cf22bf9bce | ||
|
5c41565f39 | ||
|
a8102bccd2 | ||
|
8a22f6b133 | ||
|
0e3a52b729 | ||
|
e0be5f5a54 | ||
|
1acc70dd70 | ||
|
9e3d2345f9 | ||
|
fc4b92710e | ||
|
978d4dddd5 | ||
|
0cb2aef646 | ||
|
1e4be0e16f | ||
|
fe7ba863b4 | ||
|
0795e9cbd5 | ||
|
9eb142d05b | ||
|
ce65ff5527 | ||
|
daa0441ab3 | ||
|
89103b6d13 | ||
|
1896e0f118 | ||
|
ed5577b463 | ||
|
6125d8c7b1 | ||
|
01a406eac9 | ||
|
f28bca609a | ||
|
08bfedaeb2 | ||
|
24ac43a799 | ||
|
cfaf62c95f | ||
|
931673c264 | ||
|
2a4d37ece4 | ||
|
01c45ca2a8 | ||
|
7055391230 | ||
|
fe22970973 | ||
|
eb64d186a6 | ||
|
10e2b77585 | ||
|
a34ca365f0 | ||
|
bd2f2c4c28 | ||
|
e0861cca57 | ||
|
21a9c56975 | ||
|
2990583a1f | ||
|
80f651a3eb | ||
|
a5d35730de | ||
|
b5db9fa7f1 | ||
|
b7055944a9 | ||
|
705ddc24e3 | ||
|
11cbd5eae2 | ||
|
526717ec36 | ||
|
86c8671d64 | ||
|
00dcac60d4 | ||
|
2cb81857ac | ||
|
9a652f8788 | ||
|
662386cc03 | ||
|
026d16ba68 | ||
|
efd324b82f | ||
|
73ca7765c0 | ||
|
6e6e533869 | ||
|
735f0c39f7 | ||
|
11241a2b02 | ||
|
f873bd93b7 | ||
|
4392642f03 | ||
|
f466909967 | ||
|
5a46cd6bd5 | ||
|
242b7b1055 | ||
|
1ba25ef3f9 | ||
|
a813cd1352 | ||
|
226c0c14f2 | ||
|
05cfb77430 | ||
|
810199a442 | ||
|
e0cdcd4ff8 | ||
|
df730cafb9 | ||
|
895c3ddb49 | ||
|
83b6b26e63 | ||
|
62c14b872d | ||
|
d6ad1b8bad | ||
|
c606f43a3d | ||
|
8e151b1df3 | ||
|
46a42bc7ed | ||
|
3fa09dd110 | ||
|
9050e42038 | ||
|
76cd590ca3 | ||
|
5bd9632756 | ||
|
c04f1bafde | ||
|
9f2ed487b2 | ||
|
484fdfe4b6 | ||
|
e7c745aac8 | ||
|
67d07eb450 | ||
|
d1c2cf9c6c | ||
|
c008e0e2b6 | ||
|
29d08353ec | ||
|
1ba07d4b7d | ||
|
cb86194f8f | ||
|
f980521c8e | ||
|
e7cae30bd8 | ||
|
29bb93bc29 | ||
|
306664fb6a | ||
|
7382145318 | ||
|
4f3c418202 | ||
|
f5fe86dd78 | ||
|
2a593da06c | ||
|
24e0777ebd | ||
|
3c470575f4 | ||
|
8a8cde0ce3 | ||
|
597da2d861 | ||
|
f9c53f39f4 | ||
|
56e67088a9 | ||
|
354db6b46f | ||
|
0c773a38f4 | ||
|
4bbc5b2ff8 | ||
|
1b17423343 | ||
|
83f60d6bd7 | ||
|
9ea1f845a8 | ||
|
29bdc0aa45 | ||
|
137ffc992f | ||
|
34f8f9472f | ||
|
0f1fa3d385 | ||
|
9251309c3f | ||
|
01f600cdd3 | ||
|
c360a1c840 | ||
|
3aa31d9135 | ||
|
ee873b64f6 | ||
|
248be5805b | ||
|
6bb83abf84 | ||
|
78821bbc80 | ||
|
bcd61c9621 | ||
|
2d7d2c116d | ||
|
0b0883db68 | ||
|
de79ed364e | ||
|
627e62f507 | ||
|
c9aee2e853 | ||
|
03ada87e90 | ||
|
4666e06910 | ||
|
cd9e600955 | ||
|
df3a6ce2ae | ||
|
3ecdfb6374 | ||
|
7470536606 | ||
|
dd86599d5b | ||
|
b956b06f6d | ||
|
d48e3aaa27 | ||
|
03f17310bc | ||
|
6142a167e9 | ||
|
d31f3c2482 | ||
|
b5a50d8281 | ||
|
16938ab83a | ||
|
279b1e00a7 | ||
|
40817a2f2a | ||
|
6187be0c59 | ||
|
373209640d | ||
|
0c39719d12 | ||
|
a8616ff10d | ||
|
defebcf8e5 | ||
|
6e411eb243 | ||
|
31ef245eeb | ||
|
bf4d1f8693 | ||
|
6deb12bdc8 | ||
|
0b26628232 | ||
|
9c55f5df59 | ||
|
b857388796 | ||
|
dd3043722e | ||
|
50a5ef3e7d | ||
|
c935fe36a8 | ||
|
62e6363914 | ||
|
dd84b9f8f8 | ||
|
3f1e4b5a3d | ||
|
60cb608956 | ||
|
a0df085b81 | ||
|
9f69029ef0 | ||
|
c5c4cac6c3 | ||
|
13952c5430 | ||
|
298aff24f5 | ||
|
986cc4c794 | ||
|
c2f890879c | ||
|
1407ca4392 | ||
|
7b1177adbc | ||
|
c39fc12729 | ||
|
4b610329bd | ||
|
54aca07ca1 | ||
|
10a5dc827b | ||
|
c35e3a1550 | ||
|
eb24a23568 | ||
|
98c4358615 | ||
|
bb2705128c | ||
|
b848f1cc52 | ||
|
dfaf3405b2 | ||
|
a0cd8a4fc0 | ||
|
7018cec47d | ||
|
d195203865 | ||
|
b96397f6eb | ||
|
4f3f74f016 | ||
|
16fd551176 | ||
|
fa4aa364e1 | ||
|
1589ca8ddb | ||
|
0d8b5efaa1 | ||
|
44e6fbd067 | ||
|
b125f06e70 | ||
|
51d23b5b33 | ||
|
1c68437f3c | ||
|
48698a61ad | ||
|
84745b0537 | ||
|
cc2c818341 | ||
|
b860b6da4f | ||
|
3a2367036c | ||
|
9401a810f0 | ||
|
8d24d89abf | ||
|
78091aeb46 | ||
|
941229b6c9 | ||
|
f9b758b5bf | ||
|
c4e4e8582d | ||
|
91417d58c6 | ||
|
d1df4740d5 | ||
|
e80cf48484 | ||
|
cb9c084d30 | ||
|
0a49ae3ac8 | ||
|
ceef5dc682 | ||
|
e43a173f46 | ||
|
ba1318cc45 | ||
|
fa1f10dc77 | ||
|
c4a2e7657f | ||
|
2cc878da81 | ||
|
bfa14a0d66 | ||
|
5ecb55ad1e | ||
|
73f8774aa8 | ||
|
b52d63dfbf | ||
|
083210eb40 | ||
|
14f2dcc357 | ||
|
e5e4fde1dc | ||
|
7d4a10f521 | ||
|
dbdf48777c | ||
|
7cab83a8f2 | ||
|
8dd8645b61 | ||
|
466f77aabe | ||
|
fbe95fe51e | ||
|
ad69a4f495 | ||
|
a931184edf | ||
|
c1b93247e3 | ||
|
75f2d5c8dd | ||
|
42f4f956e0 | ||
|
dc450e6192 | ||
|
5f4c46f402 | ||
|
d555d24d94 | ||
|
5c45d1e0d9 | ||
|
e0d84e1fee | ||
|
b3d7dfd603 | ||
|
7750d179be | ||
|
0c24bd4846 | ||
|
bb7a1c7a27 | ||
|
982f9c94f6 | ||
|
351bc57554 | ||
|
d045947ea7 | ||
|
96e074d030 | ||
|
e6c2ebedde | ||
|
ce879634cc | ||
|
dfc075515d | ||
|
caedfea74b | ||
|
8e28dd4ad7 | ||
|
ed1ea5a755 | ||
|
aede26d5e1 | ||
|
3b89cf1fcb | ||
|
834a401122 | ||
|
a00860ee1d | ||
|
a6d31ba0b4 | ||
|
bc7d849362 | ||
|
b131d3bba0 | ||
|
5ad9d31bd8 | ||
|
d26b2a151f | ||
|
5b194964a9 | ||
|
9deeb62e77 | ||
|
dba2f05b13 | ||
|
5d12c22f44 | ||
|
7205690bf5 | ||
|
359244e996 | ||
|
d2ef4b5a61 | ||
|
a210d89a55 | ||
|
0a6e836762 | ||
|
8cde0e5d72 | ||
|
07f5d708dd | ||
|
f53a77d311 | ||
|
a8b1550e7e | ||
|
20ffd22110 | ||
|
b06f65d2e9 | ||
|
f922371fa8 | ||
|
03323ae998 | ||
|
2b7e5a3453 | ||
|
b7a91e0557 | ||
|
fd5c5ee2df | ||
|
d6742be404 | ||
|
5812c994b1 | ||
|
b583adac37 | ||
|
e643a04ee5 | ||
|
4ac0d6f5ef | ||
|
88ec00bcf4 | ||
|
f06b43b2a4 | ||
|
e0ad48cd97 | ||
|
8bf298ac14 | ||
|
4376a50f1f | ||
|
f847d47daa | ||
|
de336ef86e | ||
|
224369155e | ||
|
165e3628be | ||
|
22c3b02b3f | ||
|
2db89355f1 | ||
|
8dfe9edc00 | ||
|
f5ceb91e0f | ||
|
eed3295495 | ||
|
816f6f81c6 | ||
|
e8a2596841 | ||
|
da1ed8f5c3 | ||
|
5125fc2f27 | ||
|
b4ad3b9789 | ||
|
f12031ecba | ||
|
76df51bfde | ||
|
54c026f5db | ||
|
ab5b3a468e | ||
|
c755894af3 | ||
|
1340ef8935 | ||
|
fdd38cddd8 | ||
|
00694e9878 | ||
|
76f50df2cc | ||
|
b7f771a70f | ||
|
3c225fcad7 | ||
|
f89544fd70 | ||
|
d3926a7f86 | ||
|
bcb150eb06 | ||
|
aee3216ca1 | ||
|
9e9655b556 | ||
|
cda6bb9f14 | ||
|
c4436cef80 | ||
|
798f14bda5 | ||
|
f42ae45f4c | ||
|
77a1f388db | ||
|
4050e45e82 | ||
|
5031a313b7 | ||
|
c8d8ee1fff | ||
|
7d0c952102 | ||
|
7d5c4adc8d | ||
|
10c77d7979 | ||
|
de4c2e782e | ||
|
494992dab8 | ||
|
e59dbf4f96 | ||
|
c65b3bf1cb | ||
|
dac5f695b3 | ||
|
0ea1959b7e | ||
|
13fd0b252c | ||
|
f942e0fd52 | ||
|
bcec358554 | ||
|
cd1542363d | ||
|
d95dd07c56 | ||
|
e76110b4b1 | ||
|
448a2d3298 | ||
|
b4c43ae771 | ||
|
6112993919 | ||
|
81bbe870f1 | ||
|
8c18caf574 | ||
|
11dbc3c50b | ||
|
718f832864 | ||
|
1b634bbb74 | ||
|
05b0a46961 | ||
|
b6ebe53ffb | ||
|
1e37d8dc42 | ||
|
af09de142d | ||
|
0a70c5c4ab | ||
|
49092ece96 |
112 changed files with 10964 additions and 962 deletions
|
@ -3,10 +3,10 @@
|
|||
# Deploy a jar, source jar, and javadoc jar to Sonatype's snapshot repo.
|
||||
#
|
||||
# Adapted from https://coderwall.com/p/9b_lfq and
|
||||
# http://benlimmer.com/2013/12/26/automatically-publish-javadoc-to-gh-pages-with-travis-ci/
|
||||
# https://benlimmer.com/2013/12/26/automatically-publish-javadoc-to-gh-pages-with-travis-ci/
|
||||
|
||||
SLUG="square/moshi"
|
||||
JDK="oraclejdk8"
|
||||
JDK="openjdk8"
|
||||
BRANCH="master"
|
||||
|
||||
set -e
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -12,6 +12,7 @@ lib
|
|||
target
|
||||
pom.xml.*
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
|
||||
.idea
|
||||
*.iml
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
language: java
|
||||
|
||||
jdk:
|
||||
- oraclejdk7
|
||||
- oraclejdk8
|
||||
- openjdk8
|
||||
|
||||
after_success:
|
||||
- .buildscript/deploy_snapshot.sh
|
||||
|
@ -19,8 +18,6 @@ branches:
|
|||
notifications:
|
||||
email: false
|
||||
|
||||
sudo: false
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.m2
|
||||
|
|
163
CHANGELOG.md
163
CHANGELOG.md
|
@ -1,6 +1,160 @@
|
|||
Change Log
|
||||
==========
|
||||
|
||||
## Version 1.8.0
|
||||
|
||||
_2018-11-09_
|
||||
|
||||
* New: Support JSON objects that include type information in the JSON. The new
|
||||
`PolymorphicJsonAdapterFactory` writes a type field when encoding, and reads it when decoding.
|
||||
* New: Fall back to the reflection-based `KotlinJsonAdapterFactory` if it is enabled and a
|
||||
generated adapter is not found. This makes it possible to use reflection-based JSON adapters in
|
||||
development (so you don't have to wait for code to be regenerated on every build) and generated
|
||||
JSON adapters in production (so you don't need the kotlin-reflect library).
|
||||
* New: The `peekJson()` method on `JsonReader` let you read ahead on a JSON stream without
|
||||
consuming it. This builds on Okio's new `Buffer.peek()` API.
|
||||
* New: The `beginFlatten()` and `endFlatten()` methods on `JsonWriter` suppress unwanted nesting
|
||||
when composing adapters. Previously it was necessary to flatten objects in memory before writing.
|
||||
* New: Upgrade to Okio 1.16.0. We don't yet require Kotlin-friendly Okio 2.1 but Moshi works fine
|
||||
with that release.
|
||||
|
||||
```kotlin
|
||||
implementation("com.squareup.okio:okio:1.16.0")
|
||||
```
|
||||
|
||||
* Fix: Don't return partially-constructed adapters when using a Moshi instance concurrently.
|
||||
* Fix: Eliminate warnings and errors in generated `.kt` triggered by type variance, primitive
|
||||
types, and required values.
|
||||
* Fix: Improve the supplied rules (`moshi.pro`) to better retain symbols used by Moshi. We
|
||||
recommend R8 when shrinking code.
|
||||
* Fix: Remove code generation companion objects. This API was neither complete nor necessary.
|
||||
|
||||
|
||||
## Version 1.7.0
|
||||
|
||||
_2018-09-24_
|
||||
|
||||
* New: `EnumJsonAdapter` makes it easy to specify a fallback value for unknown enum constants.
|
||||
By default Moshi throws an `JsonDataException` if it reads an unknown enum constant. With this
|
||||
you can specify a fallback value or null.
|
||||
|
||||
```java
|
||||
new Moshi.Builder()
|
||||
.add(EnumJsonAdapter.create(IsoCurrency.class)
|
||||
.withUnknownFallback(IsoCurrency.USD))
|
||||
.build();
|
||||
```
|
||||
|
||||
Note that this adapter is in the optional `moshi-adapters` module.
|
||||
|
||||
```groovy
|
||||
implementation 'com.squareup.moshi:moshi-adapters:1.7.0'
|
||||
```
|
||||
|
||||
* New: Embed R8/ProGuard rules in the `.jar` file.
|
||||
* New: Use `@CheckReturnValue` in more places. We hope this will encourage you to use `skipName()`
|
||||
instead of `nextName()` for better performance!
|
||||
* New: Forbid automatic encoding of platform classes in `androidx`. As with `java.*`, `android.*`,
|
||||
and `kotlin.*` Moshi wants you to specify how to encode platform types.
|
||||
* New: Improve error reporting when creating an adapter fails.
|
||||
* New: Upgrade to Okio 1.15.0. We don't yet require Kotlin-friendly Okio 2.x but Moshi works fine
|
||||
with that release.
|
||||
|
||||
```groovy
|
||||
implementation 'com.squareup.okio:okio:1.15.0'
|
||||
```
|
||||
|
||||
* Fix: Return false from `JsonReader.hasNext()` at document's end.
|
||||
* Fix: Improve code gen to handle several broken cases. Our generated adapters had problems with
|
||||
nulls, nested parameterized types, private transient properties, generic type aliases, fields
|
||||
with dollar signs in their names, and named companion objects.
|
||||
|
||||
|
||||
## Version 1.6.0
|
||||
|
||||
_2018-05-14_
|
||||
|
||||
* **Moshi now supports codegen for Kotlin.** We've added a new annotation processor that generates
|
||||
a small and fast JSON adapter for your Kotlin types. It can be used on its own or with the
|
||||
existing `KotlinJsonAdapterFactory` adapter.
|
||||
|
||||
* **Moshi now resolves all type parameters.** Previously Moshi wouldn't resolve type parameters on
|
||||
top-level classes.
|
||||
|
||||
* New: Support up to 255 levels of nesting when reading and writing JSON. Previously Moshi would
|
||||
reject JSON input that contained more than 32 levels of nesting.
|
||||
* New: Write encoded JSON to a stream with `JsonWriter.value(BufferedSource)`. Use this to emit a
|
||||
JSON value without decoding it first.
|
||||
* New: `JsonAdapter.nonNull()` returns a new JSON adapter that forbids explicit nulls in the JSON
|
||||
body. Use this to detect and fail eagerly on unwanted nulls.
|
||||
* New: `JsonReader.skipName()` is like `nextName()` but it avoids allocating when a name is
|
||||
unknown. Use this when `JsonReader.selectName()` returns -1.
|
||||
* New: Automatic module name of `com.squareup.moshi` for use with the Java Platform Module System.
|
||||
This moves moshi-adapters into its own `.adapters` package and forwards the existing adapter. It
|
||||
moves the moshi-kotlin into its own `.kotlin.reflect` package and forwards the existing adapter.
|
||||
* New: Upgrade to Okio 1.14.0.
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.squareup.okio</groupId>
|
||||
<artifactId>okio</artifactId>
|
||||
<version>1.14.0</version>
|
||||
</dependency>
|
||||
|
||||
com.squareup.okio:okio:1.14.0
|
||||
```
|
||||
|
||||
* Fix: Fail fast if there are trailing non-whitespace characters in the JSON passed to
|
||||
`JsonAdapter.fromJson(String)`. Previously such data was ignored!
|
||||
* Fix: Fail fast when Kotlin types are abstract, inner, or object instances.
|
||||
* Fix: Fail fast if `name()` is called out of sequence.
|
||||
* Fix: Handle asymmetric `Type.equals()` methods when doing type comparisons. Previously it was
|
||||
possible that a registered type adapter would not be used because its `Type.equals()` method was
|
||||
not consistent with a user-provided type.
|
||||
* Fix: `JsonValueReader.selectString()` now returns -1 for non-strings instead of throwing.
|
||||
* Fix: Permit reading numbers as strings when the `JsonReader` was created from a JSON value. This
|
||||
was always supported when reading from a stream but broken when reading from a decoded value.
|
||||
* Fix: Delegate to user-adapters in the adapter for Object.class. Previously when Moshi encountered
|
||||
an opaque Object it would only use the built-in adapters. With this change user-installed
|
||||
adapters for types like `String` will always be honored.
|
||||
|
||||
## Version 1.5.0
|
||||
|
||||
_2017-05-14_
|
||||
|
||||
* **Moshi now uses `@Nullable` to annotate all possibly-null values.** We've added a compile-time
|
||||
dependency on the JSR 305 annotations. This is a [provided][maven_provided] dependency and does
|
||||
not need to be included in your build configuration, `.jar` file, or `.apk`. We use
|
||||
`@ParametersAreNonnullByDefault` and all parameters and return types are never null unless
|
||||
explicitly annotated `@Nullable`.
|
||||
|
||||
* **Warning: Moshi APIs in this update are source-incompatible for Kotlin callers.** Nullability
|
||||
was previously ambiguous and lenient but now the compiler will enforce strict null checks.
|
||||
|
||||
* **Kotlin models are now supported via the `moshi-kotlin` extension.** `KotlinJsonAdapterFactory`
|
||||
is the best way to use Kotlin with Moshi. It honors default values and is null-safe. Kotlin
|
||||
users that don't use this factory should write custom adapters for their JSON types. Otherwise
|
||||
Moshi cannot properly initialize delegated properties of the objects it decodes.
|
||||
|
||||
* New: Upgrade to Okio 1.13.0.
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.squareup.okio</groupId>
|
||||
<artifactId>okio</artifactId>
|
||||
<version>1.13.0</version>
|
||||
</dependency>
|
||||
|
||||
com.squareup.okio:okio:1.13.0
|
||||
```
|
||||
|
||||
* New: You may now declare delegates in `@ToJson` and `@FromJson` methods. If one of the arguments
|
||||
to the method is a `JsonAdapter` of the same type, that will be the next eligible adapter for
|
||||
that type. This may be useful for composing adapters.
|
||||
* New: `Types.equals(Type, Type)` makes it easier to compare types in `JsonAdapter.Factory`.
|
||||
* Fix: Retain the sign on negative zero.
|
||||
|
||||
|
||||
## Version 1.4.0
|
||||
|
||||
_2017-02-04_
|
||||
|
@ -33,8 +187,8 @@ your application classes.
|
|||
format of the encoded JSON.
|
||||
* New: `JsonReader.selectName()` and `selectString()` optimize decoding JSON with known names and
|
||||
values.
|
||||
* New: `Types.nextAnnotations()` and `Types.createJsonQualifierImplementation()` reduce the amount
|
||||
of code required to implement a custom `JsonAdapter.Factory`.
|
||||
* New: `Types.nextAnnotations()` reduces the amount of code required to implement a custom
|
||||
`JsonAdapter.Factory`.
|
||||
* Fix: Don't fail on large longs that have a fractional component like `9223372036854775806.0`.
|
||||
|
||||
## Version 1.3.1
|
||||
|
@ -72,7 +226,7 @@ _2016-10-15_
|
|||
`StackOverflowError` due to excessive recursion.
|
||||
* Fix: Require enclosed types to specify their enclosing type with
|
||||
`Types.newParameterizedTypeWithOwner()`. Previously this API did not exist and looking up
|
||||
adapters for enclosed parameterized types as not possible.
|
||||
adapters for enclosed parameterized types was not possible.
|
||||
* Fix: Fail on invalid escapes. Previously any character could be escaped. With this fix only
|
||||
characters permitted to be escaped may be escaped. Use `JsonReader.setLenient(true)` to read
|
||||
JSON documents that escape characters that should not be escaped.
|
||||
|
@ -134,7 +288,7 @@ _2015-09-27_
|
|||
code should keep a reference to required adapters in a field.
|
||||
* New: The `Types` factory class makes it possible to compose types like `List<Card>` or
|
||||
`Map<String, Integer>`. This is useful to look up JSON adapters for parameterized types.
|
||||
* New: `JsonAdapter.failOnUnknown()` returns a new JSON adapter that throws if an unknonw value is
|
||||
* New: `JsonAdapter.failOnUnknown()` returns a new JSON adapter that throws if an unknown value is
|
||||
encountered on the stream. Use this in development and debug builds to detect typos in field
|
||||
names. This feature shouldn’t be used in production because it makes migrations very difficult.
|
||||
|
||||
|
@ -158,3 +312,4 @@ _2015-06-16_
|
|||
[rfc_7159]: https://tools.ietf.org/html/rfc7159
|
||||
[gson]: https://github.com/google/gson
|
||||
[jackson]: http://wiki.fasterxml.com/JacksonHome
|
||||
[maven_provided]: https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html
|
||||
|
|
159
README.md
159
README.md
|
@ -120,7 +120,7 @@ Moshi moshi = new Moshi.Builder()
|
|||
.build();
|
||||
```
|
||||
|
||||
Voila:
|
||||
Voilà:
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -213,20 +213,53 @@ JsonAdapter<Event> jsonAdapter = moshi.adapter(Event.class);
|
|||
Event event = jsonAdapter.fromJson(json);
|
||||
```
|
||||
|
||||
### Adapter convenience methods
|
||||
|
||||
Moshi provides a number of convenience methods for `JsonAdapter` objects:
|
||||
- `nullSafe()`
|
||||
- `nonNull()`
|
||||
- `lenient()`
|
||||
- `failOnUnknown()`
|
||||
- `indent()`
|
||||
- `serializeNulls()`
|
||||
|
||||
These factory methods wrap an existing `JsonAdapter` into additional functionality.
|
||||
For example, if you have an adapter that doesn't support nullable values, you can use `nullSafe()` to make it null safe:
|
||||
|
||||
```java
|
||||
String dateJson = "\"2018-11-26T11:04:19.342668Z\"";
|
||||
String nullDateJson = "null";
|
||||
|
||||
// RFC 3339 date adapter, doesn't support null by default
|
||||
// See also: https://github.com/square/moshi/tree/master/adapters
|
||||
JsonAdapter<Date> adapter = new Rfc3339DateJsonAdapter();
|
||||
|
||||
Date date = adapter.fromJson(dateJson);
|
||||
System.out.println(date); // Mon Nov 26 12:04:19 CET 2018
|
||||
|
||||
Date nullDate = adapter.fromJson(nullDateJson);
|
||||
// Exception, com.squareup.moshi.JsonDataException: Expected a string but was NULL at path $
|
||||
|
||||
Date nullDate = adapter.nullSafe().fromJson(nullDateJson);
|
||||
System.out.println(nullDate); // null
|
||||
```
|
||||
|
||||
In contrast to `nullSafe()` there is `nonNull()` to make an adapter refuse null values. Refer to the Moshi JavaDoc for details on the various methods.
|
||||
|
||||
### Parse JSON Arrays
|
||||
|
||||
Say we have a JSON string of this structure:
|
||||
|
||||
```java
|
||||
```json
|
||||
[
|
||||
{
|
||||
"rank": "4",
|
||||
"suit": "CLUBS"
|
||||
},
|
||||
{
|
||||
"rank": "A",
|
||||
"suit": "HEARTS"
|
||||
}
|
||||
{
|
||||
"rank": "4",
|
||||
"suit": "CLUBS"
|
||||
},
|
||||
{
|
||||
"rank": "A",
|
||||
"suit": "HEARTS"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
@ -472,26 +505,121 @@ public final class BlackjackHand {
|
|||
}
|
||||
```
|
||||
|
||||
Kotlin
|
||||
------
|
||||
|
||||
Moshi is a great JSON library for Kotlin. It understands Kotlin’s non-nullable types and default
|
||||
parameter values. When you use Kotlin with Moshi you may use reflection, codegen, or both.
|
||||
|
||||
#### Reflection
|
||||
|
||||
The reflection adapter uses Kotlin’s reflection library to convert your Kotlin classes to and from
|
||||
JSON. Enable it by adding the `KotlinJsonAdapterFactory` to your `Moshi.Builder`:
|
||||
|
||||
```kotlin
|
||||
val moshi = Moshi.Builder()
|
||||
// ... add your own JsonAdapters and factories ...
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
```
|
||||
|
||||
Moshi’s adapters are ordered by precedence, so you always want to add the Kotlin adapter after your
|
||||
own custom adapters. Otherwise the `KotlinJsonAdapterFactory` will take precedence and your custom
|
||||
adapters will not be called.
|
||||
|
||||
The reflection adapter requires the following additional dependency:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi-kotlin</artifactId>
|
||||
<version>1.8.0</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
```kotlin
|
||||
implementation("com.squareup.moshi:moshi-kotlin:1.8.0")
|
||||
```
|
||||
|
||||
Note that the reflection adapter transitively depends on the `kotlin-reflect` library which is a
|
||||
2.5 MiB .jar file.
|
||||
|
||||
#### Codegen
|
||||
|
||||
Moshi’s Kotlin codegen support is an annotation processor. It generates a small and fast adapter for
|
||||
each of your Kotlin classes at compile time. Enable it by annotating each class that you want to
|
||||
encode as JSON:
|
||||
|
||||
```kotlin
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class BlackjackHand(
|
||||
val hidden_card: Card,
|
||||
val visible_cards: List<Card>
|
||||
)
|
||||
```
|
||||
|
||||
The codegen adapter requires that your Kotlin types and their properties be either `internal` or
|
||||
`public` (this is Kotlin’s default visibility).
|
||||
|
||||
Kotlin codegen has no additional runtime dependency. You’ll need to [enable kapt][kapt] and then
|
||||
add the following to your build to enable the annotation processor:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi-kotlin-codegen</artifactId>
|
||||
<version>1.8.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
```kotlin
|
||||
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.8.0")
|
||||
```
|
||||
|
||||
You must also have the `kotlin-stdlib` dependency on the classpath during compilation in order for
|
||||
the compiled code to have the required metadata annotations that Moshi's processor looks for.
|
||||
|
||||
#### Limitations
|
||||
|
||||
If your Kotlin class has a superclass, it must also be a Kotlin class. Neither reflection or codegen
|
||||
support Kotlin types with Java supertypes or Java types with Kotlin supertypes. If you need to
|
||||
convert such classes to JSON you must create a custom type adapter.
|
||||
|
||||
The JSON encoding of Kotlin types is the same whether using reflection or codegen. Prefer codegen
|
||||
for better performance and to avoid the `kotlin-reflect` dependency; prefer reflection to convert
|
||||
both private and protected properties. If you have configured both, generated adapters will be used
|
||||
on types that are annotated `@JsonClass(generateAdapter = true)`.
|
||||
|
||||
Download
|
||||
--------
|
||||
|
||||
Download [the latest JAR][dl] or depend via Maven:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi</artifactId>
|
||||
<version>1.3.1</version>
|
||||
<version>1.8.0</version>
|
||||
</dependency>
|
||||
```
|
||||
or Gradle:
|
||||
```groovy
|
||||
compile 'com.squareup.moshi:moshi:1.3.1'
|
||||
```kotlin
|
||||
implementation("com.squareup.moshi:moshi:1.8.0")
|
||||
```
|
||||
|
||||
Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap].
|
||||
|
||||
|
||||
R8 / ProGuard
|
||||
--------
|
||||
|
||||
If you are using R8 or ProGuard add the options from [this file](https://github.com/square/moshi/blob/master/moshi/src/main/resources/META-INF/proguard/moshi.pro). If using Android, this requires Android Gradle Plugin 3.2.0+.
|
||||
|
||||
The `moshi-kotlin` artifact additionally requires the options from [this file](https://github.com/square/moshi/blob/master/kotlin/reflect/src/main/resources/META-INF/proguard/moshi-kotlin.pro)
|
||||
|
||||
You might also need rules for Okio which is a dependency of this library.
|
||||
|
||||
License
|
||||
--------
|
||||
|
||||
|
@ -510,9 +638,10 @@ License
|
|||
limitations under the License.
|
||||
|
||||
|
||||
[dl]: https://search.maven.org/remote_content?g=com.squareup.moshi&a=moshi&v=LATEST
|
||||
[dl]: https://search.maven.org/classic/remote_content?g=com.squareup.moshi&a=moshi&v=LATEST
|
||||
[snap]: https://oss.sonatype.org/content/repositories/snapshots/com/squareup/moshi/
|
||||
[okio]: https://github.com/square/okio/
|
||||
[okhttp]: https://github.com/square/okhttp/
|
||||
[gson]: https://github.com/google/gson/
|
||||
[javadoc]: http://square.github.io/moshi/1.x/moshi/
|
||||
[javadoc]: https://square.github.io/moshi/1.x/moshi/
|
||||
[kapt]: https://kotlinlang.org/docs/reference/kapt.html
|
||||
|
|
37
adapters/README.md
Normal file
37
adapters/README.md
Normal file
|
@ -0,0 +1,37 @@
|
|||
Adapters
|
||||
===================
|
||||
|
||||
Prebuilt Moshi `JsonAdapter`s for various things, such as `Rfc3339DateJsonAdapter` for parsing `java.util.Date`s
|
||||
|
||||
To use, supply an instance of your desired converter when building your `Moshi` instance.
|
||||
|
||||
```java
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(Date.class, new Rfc3339DateJsonAdapter())
|
||||
//etc
|
||||
.build();
|
||||
```
|
||||
|
||||
Download
|
||||
--------
|
||||
|
||||
Download [the latest JAR][1] or grab via [Maven][2]:
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi-adapters</artifactId>
|
||||
<version>latest.version</version>
|
||||
</dependency>
|
||||
```
|
||||
or [Gradle][2]:
|
||||
```groovy
|
||||
implementation 'com.squareup.moshi:moshi-adapters:latest.version'
|
||||
```
|
||||
|
||||
Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap].
|
||||
|
||||
|
||||
|
||||
[1]: https://search.maven.org/remote_content?g=com.squareup.moshi&a=moshi-adapters&v=LATEST
|
||||
[2]: http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.squareup.moshi%22%20a%3A%22moshi-adapters%22
|
||||
[snap]: https://oss.sonatype.org/content/repositories/snapshots/com/squareup/moshi/moshi-adapters/
|
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi-parent</artifactId>
|
||||
<version>1.4.0</version>
|
||||
<version>1.9.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>moshi-adapters</artifactId>
|
||||
|
@ -17,6 +17,11 @@
|
|||
<artifactId>moshi</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.findbugs</groupId>
|
||||
<artifactId>jsr305</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
|
@ -28,4 +33,20 @@
|
|||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifestEntries>
|
||||
<Automatic-Module-Name>com.squareup.moshi.adapters</Automatic-Module-Name>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
|
|
@ -19,17 +19,18 @@ import java.io.IOException;
|
|||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Formats dates using <a href="https://www.ietf.org/rfc/rfc3339.txt">RFC 3339</a>, which is
|
||||
* formatted like {@code 2015-09-26T18:23:50.250Z}.
|
||||
* @deprecated this class moved to avoid a package name conflict in the Java Platform Module System.
|
||||
* The new class is {@code com.squareup.moshi.adapters.Rfc3339DateJsonAdapter}.
|
||||
*/
|
||||
public final class Rfc3339DateJsonAdapter extends JsonAdapter<Date> {
|
||||
@Override public synchronized Date fromJson(JsonReader reader) throws IOException {
|
||||
String string = reader.nextString();
|
||||
return Iso8601Utils.parse(string);
|
||||
com.squareup.moshi.adapters.Rfc3339DateJsonAdapter delegate
|
||||
= new com.squareup.moshi.adapters.Rfc3339DateJsonAdapter();
|
||||
|
||||
@Override public Date fromJson(JsonReader reader) throws IOException {
|
||||
return delegate.fromJson(reader);
|
||||
}
|
||||
|
||||
@Override public synchronized void toJson(JsonWriter writer, Date value) throws IOException {
|
||||
String string = Iso8601Utils.format(value);
|
||||
writer.value(string);
|
||||
@Override public void toJson(JsonWriter writer, Date value) throws IOException {
|
||||
delegate.toJson(writer, value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.adapters;
|
||||
|
||||
import com.squareup.moshi.Json;
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.JsonDataException;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.JsonWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* A JsonAdapter for enums that allows having a fallback enum value when a deserialized string does
|
||||
* not match any enum value. To use, add this as an adapter for your enum type on your {@link
|
||||
* com.squareup.moshi.Moshi.Builder Moshi.Builder}:
|
||||
*
|
||||
* <pre> {@code
|
||||
*
|
||||
* Moshi moshi = new Moshi.Builder()
|
||||
* .add(CurrencyCode.class, EnumJsonAdapter.create(CurrencyCode.class)
|
||||
* .withUnknownFallback(CurrencyCode.USD))
|
||||
* .build();
|
||||
* }</pre>
|
||||
*/
|
||||
public final class EnumJsonAdapter<T extends Enum<T>> extends JsonAdapter<T> {
|
||||
final Class<T> enumType;
|
||||
final String[] nameStrings;
|
||||
final T[] constants;
|
||||
final JsonReader.Options options;
|
||||
final boolean useFallbackValue;
|
||||
final @Nullable T fallbackValue;
|
||||
|
||||
public static <T extends Enum<T>> EnumJsonAdapter<T> create(Class<T> enumType) {
|
||||
return new EnumJsonAdapter<>(enumType, null, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new adapter for this enum with a fallback value to use when the JSON string does not
|
||||
* match any of the enum's constants. Note that this value will not be used when the JSON value is
|
||||
* null, absent, or not a string. Also, the string values are case-sensitive, and this fallback
|
||||
* value will be used even on case mismatches.
|
||||
*/
|
||||
public EnumJsonAdapter<T> withUnknownFallback(@Nullable T fallbackValue) {
|
||||
return new EnumJsonAdapter<>(enumType, fallbackValue, true);
|
||||
}
|
||||
|
||||
EnumJsonAdapter(Class<T> enumType, @Nullable T fallbackValue, boolean useFallbackValue) {
|
||||
this.enumType = enumType;
|
||||
this.fallbackValue = fallbackValue;
|
||||
this.useFallbackValue = useFallbackValue;
|
||||
try {
|
||||
constants = enumType.getEnumConstants();
|
||||
nameStrings = new String[constants.length];
|
||||
for (int i = 0; i < constants.length; i++) {
|
||||
String constantName = constants[i].name();
|
||||
Json annotation = enumType.getField(constantName).getAnnotation(Json.class);
|
||||
String name = annotation != null ? annotation.name() : constantName;
|
||||
nameStrings[i] = name;
|
||||
}
|
||||
options = JsonReader.Options.of(nameStrings);
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new AssertionError("Missing field in " + enumType.getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public @Nullable T fromJson(JsonReader reader) throws IOException {
|
||||
int index = reader.selectString(options);
|
||||
if (index != -1) return constants[index];
|
||||
|
||||
String path = reader.getPath();
|
||||
if (!useFallbackValue) {
|
||||
String name = reader.nextString();
|
||||
throw new JsonDataException("Expected one of "
|
||||
+ Arrays.asList(nameStrings) + " but was " + name + " at path " + path);
|
||||
}
|
||||
if (reader.peek() != JsonReader.Token.STRING) {
|
||||
throw new JsonDataException(
|
||||
"Expected a string but was " + reader.peek() + " at path " + path);
|
||||
}
|
||||
reader.skipValue();
|
||||
return fallbackValue;
|
||||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, T value) throws IOException {
|
||||
if (value == null) {
|
||||
throw new NullPointerException(
|
||||
"value was null! Wrap in .nullSafe() to write nullable values.");
|
||||
}
|
||||
writer.value(nameStrings[value.ordinal()]);
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
return "EnumJsonAdapter(" + enumType.getName() + ")";
|
||||
}
|
||||
}
|
|
@ -13,8 +13,9 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi;
|
||||
package com.squareup.moshi.adapters;
|
||||
|
||||
import com.squareup.moshi.JsonDataException;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
|
@ -23,7 +24,7 @@ import java.util.TimeZone;
|
|||
|
||||
/**
|
||||
* Jackson’s date formatter, pruned to Moshi's needs. Forked from this file:
|
||||
* https://github.com/FasterXML/jackson-databind/blob/master/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java
|
||||
* https://github.com/FasterXML/jackson-databind/blob/67ebf7305f492285a8f9f4de31545f5f16fc7c3a/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java
|
||||
*
|
||||
* Utilities methods for manipulating dates in iso8601 format. This is much much faster and GC
|
||||
* friendly than using SimpleDateFormat so highly suitable if you (un)serialize lots of date
|
||||
|
@ -190,7 +191,7 @@ final class Iso8601Utils {
|
|||
// If we get a ParseException it'll already have the right message/offset.
|
||||
// Other exception types can convert here.
|
||||
} catch (IndexOutOfBoundsException | IllegalArgumentException e) {
|
||||
throw new JsonDataException("Not an RFC 3339 date: " + date);
|
||||
throw new JsonDataException("Not an RFC 3339 date: " + date, e);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,301 @@
|
|||
/*
|
||||
* Copyright (C) 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.adapters;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.JsonDataException;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.JsonWriter;
|
||||
import com.squareup.moshi.Moshi;
|
||||
import com.squareup.moshi.Types;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* A JsonAdapter factory for objects that include type information in the JSON. When decoding JSON
|
||||
* Moshi uses this type information to determine which class to decode to. When encoding Moshi uses
|
||||
* the object’s class to determine what type information to include.
|
||||
*
|
||||
* <p>Suppose we have an interface, its implementations, and a class that uses them:
|
||||
*
|
||||
* <pre> {@code
|
||||
*
|
||||
* interface HandOfCards {
|
||||
* }
|
||||
*
|
||||
* class BlackjackHand extends HandOfCards {
|
||||
* Card hidden_card;
|
||||
* List<Card> visible_cards;
|
||||
* }
|
||||
*
|
||||
* class HoldemHand extends HandOfCards {
|
||||
* Set<Card> hidden_cards;
|
||||
* }
|
||||
*
|
||||
* class Player {
|
||||
* String name;
|
||||
* HandOfCards hand;
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* <p>We want to decode the following JSON into the player model above:
|
||||
*
|
||||
* <pre> {@code
|
||||
*
|
||||
* {
|
||||
* "name": "Jesse",
|
||||
* "hand": {
|
||||
* "hand_type": "blackjack",
|
||||
* "hidden_card": "9D",
|
||||
* "visible_cards": ["8H", "4C"]
|
||||
* }
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* <p>Left unconfigured, Moshi would incorrectly attempt to decode the hand object to the abstract
|
||||
* {@code HandOfCards} interface. We configure it to use the appropriate subtype instead:
|
||||
*
|
||||
* <pre> {@code
|
||||
*
|
||||
* Moshi moshi = new Moshi.Builder()
|
||||
* .add(PolymorphicJsonAdapterFactory.of(HandOfCards.class, "hand_type")
|
||||
* .withSubtype(BlackjackHand.class, "blackjack")
|
||||
* .withSubtype(HoldemHand.class, "holdem"))
|
||||
* .build();
|
||||
* }</pre>
|
||||
*
|
||||
* <p>This class imposes strict requirements on its use:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Base types may be classes or interfaces.
|
||||
* <li>Subtypes must encode as JSON objects.
|
||||
* <li>Type information must be in the encoded object. Each message must have a type label like
|
||||
* {@code hand_type} whose value is a string like {@code blackjack} that identifies which type
|
||||
* to use.
|
||||
* <li>Each type identifier must be unique.
|
||||
* </ul>
|
||||
*
|
||||
* <p>For best performance type information should be the first field in the object. Otherwise Moshi
|
||||
* must reprocess the JSON stream once it knows the object's type.
|
||||
*
|
||||
* <p>If an unknown subtype is encountered when decoding, this will throw a {@link
|
||||
* JsonDataException}. If an unknown type is encountered when encoding, this will throw an {@link
|
||||
* IllegalArgumentException}.
|
||||
*
|
||||
* <p>If you want to specify a custom unknown fallback for decoding, you can do so via
|
||||
* {@link #withDefaultValue(Object)}. This instance should be immutable, as it is shared.
|
||||
*/
|
||||
public final class PolymorphicJsonAdapterFactory<T> implements JsonAdapter.Factory {
|
||||
final Class<T> baseType;
|
||||
final String labelKey;
|
||||
final List<String> labels;
|
||||
final List<Type> subtypes;
|
||||
@Nullable final T defaultValue;
|
||||
final boolean defaultValueSet;
|
||||
|
||||
PolymorphicJsonAdapterFactory(
|
||||
Class<T> baseType,
|
||||
String labelKey,
|
||||
List<String> labels,
|
||||
List<Type> subtypes,
|
||||
@Nullable T defaultValue,
|
||||
boolean defaultValueSet) {
|
||||
this.baseType = baseType;
|
||||
this.labelKey = labelKey;
|
||||
this.labels = labels;
|
||||
this.subtypes = subtypes;
|
||||
this.defaultValue = defaultValue;
|
||||
this.defaultValueSet = defaultValueSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param baseType The base type for which this factory will create adapters. Cannot be Object.
|
||||
* @param labelKey The key in the JSON object whose value determines the type to which to map the
|
||||
* JSON object.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public static <T> PolymorphicJsonAdapterFactory<T> of(Class<T> baseType, String labelKey) {
|
||||
if (baseType == null) throw new NullPointerException("baseType == null");
|
||||
if (labelKey == null) throw new NullPointerException("labelKey == null");
|
||||
return new PolymorphicJsonAdapterFactory<>(
|
||||
baseType,
|
||||
labelKey,
|
||||
Collections.<String>emptyList(),
|
||||
Collections.<Type>emptyList(),
|
||||
null,
|
||||
false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new factory that decodes instances of {@code subtype}. When an unknown type is found
|
||||
* during encoding an {@linkplain IllegalArgumentException} will be thrown. When an unknown label
|
||||
* is found during decoding a {@linkplain JsonDataException} will be thrown.
|
||||
*/
|
||||
public PolymorphicJsonAdapterFactory<T> withSubtype(Class<? extends T> subtype, String label) {
|
||||
if (subtype == null) throw new NullPointerException("subtype == null");
|
||||
if (label == null) throw new NullPointerException("label == null");
|
||||
if (labels.contains(label)) {
|
||||
throw new IllegalArgumentException("Labels must be unique.");
|
||||
}
|
||||
List<String> newLabels = new ArrayList<>(labels);
|
||||
newLabels.add(label);
|
||||
List<Type> newSubtypes = new ArrayList<>(subtypes);
|
||||
newSubtypes.add(subtype);
|
||||
return new PolymorphicJsonAdapterFactory<>(baseType,
|
||||
labelKey,
|
||||
newLabels,
|
||||
newSubtypes,
|
||||
defaultValue,
|
||||
defaultValueSet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new factory that with default to {@code defaultValue} upon decoding of unrecognized
|
||||
* labels. The default value should be immutable.
|
||||
*/
|
||||
public PolymorphicJsonAdapterFactory<T> withDefaultValue(@Nullable T defaultValue) {
|
||||
return new PolymorphicJsonAdapterFactory<>(baseType,
|
||||
labelKey,
|
||||
labels,
|
||||
subtypes,
|
||||
defaultValue,
|
||||
true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
if (Types.getRawType(type) != baseType || !annotations.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<JsonAdapter<Object>> jsonAdapters = new ArrayList<>(subtypes.size());
|
||||
for (int i = 0, size = subtypes.size(); i < size; i++) {
|
||||
jsonAdapters.add(moshi.adapter(subtypes.get(i)));
|
||||
}
|
||||
|
||||
return new PolymorphicJsonAdapter(labelKey,
|
||||
labels,
|
||||
subtypes,
|
||||
jsonAdapters,
|
||||
defaultValue,
|
||||
defaultValueSet
|
||||
).nullSafe();
|
||||
}
|
||||
|
||||
static final class PolymorphicJsonAdapter extends JsonAdapter<Object> {
|
||||
final String labelKey;
|
||||
final List<String> labels;
|
||||
final List<Type> subtypes;
|
||||
final List<JsonAdapter<Object>> jsonAdapters;
|
||||
@Nullable final Object defaultValue;
|
||||
final boolean defaultValueSet;
|
||||
|
||||
/** Single-element options containing the label's key only. */
|
||||
final JsonReader.Options labelKeyOptions;
|
||||
/** Corresponds to subtypes. */
|
||||
final JsonReader.Options labelOptions;
|
||||
|
||||
PolymorphicJsonAdapter(String labelKey,
|
||||
List<String> labels,
|
||||
List<Type> subtypes,
|
||||
List<JsonAdapter<Object>> jsonAdapters,
|
||||
@Nullable Object defaultValue,
|
||||
boolean defaultValueSet) {
|
||||
this.labelKey = labelKey;
|
||||
this.labels = labels;
|
||||
this.subtypes = subtypes;
|
||||
this.jsonAdapters = jsonAdapters;
|
||||
this.defaultValue = defaultValue;
|
||||
this.defaultValueSet = defaultValueSet;
|
||||
|
||||
this.labelKeyOptions = JsonReader.Options.of(labelKey);
|
||||
this.labelOptions = JsonReader.Options.of(labels.toArray(new String[0]));
|
||||
}
|
||||
|
||||
@Override public Object fromJson(JsonReader reader) throws IOException {
|
||||
JsonReader peeked = reader.peekJson();
|
||||
peeked.setFailOnUnknown(false);
|
||||
int labelIndex;
|
||||
try {
|
||||
labelIndex = labelIndex(peeked);
|
||||
} finally {
|
||||
peeked.close();
|
||||
}
|
||||
if (labelIndex == -1) {
|
||||
reader.skipValue();
|
||||
return defaultValue;
|
||||
}
|
||||
return jsonAdapters.get(labelIndex).fromJson(reader);
|
||||
}
|
||||
|
||||
private int labelIndex(JsonReader reader) throws IOException {
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
if (reader.selectName(labelKeyOptions) == -1) {
|
||||
reader.skipName();
|
||||
reader.skipValue();
|
||||
continue;
|
||||
}
|
||||
|
||||
int labelIndex = reader.selectString(labelOptions);
|
||||
if (labelIndex == -1 && !defaultValueSet) {
|
||||
throw new JsonDataException("Expected one of "
|
||||
+ labels
|
||||
+ " for key '"
|
||||
+ labelKey
|
||||
+ "' but found '"
|
||||
+ reader.nextString()
|
||||
+ "'. Register a subtype for this label.");
|
||||
}
|
||||
return labelIndex;
|
||||
}
|
||||
|
||||
throw new JsonDataException("Missing label for " + labelKey);
|
||||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, Object value) throws IOException {
|
||||
Class<?> type = value.getClass();
|
||||
int labelIndex = subtypes.indexOf(type);
|
||||
if (labelIndex == -1) {
|
||||
throw new IllegalArgumentException("Expected one of "
|
||||
+ subtypes
|
||||
+ " but found "
|
||||
+ value
|
||||
+ ", a "
|
||||
+ value.getClass()
|
||||
+ ". Register this subtype.");
|
||||
}
|
||||
JsonAdapter<Object> adapter = jsonAdapters.get(labelIndex);
|
||||
writer.beginObject();
|
||||
writer.name(labelKey).value(labels.get(labelIndex));
|
||||
int flattenToken = writer.beginFlatten();
|
||||
adapter.toJson(writer, value);
|
||||
writer.endFlatten(flattenToken);
|
||||
writer.endObject();
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
return "PolymorphicJsonAdapter(" + labelKey + ")";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright (C) 2015 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.adapters;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.JsonWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Formats dates using <a href="https://www.ietf.org/rfc/rfc3339.txt">RFC 3339</a>, which is
|
||||
* formatted like {@code 2015-09-26T18:23:50.250Z}. To use, add this as an adapter for {@code
|
||||
* Date.class} on your {@link com.squareup.moshi.Moshi.Builder Moshi.Builder}:
|
||||
*
|
||||
* <pre> {@code
|
||||
*
|
||||
* Moshi moshi = new Moshi.Builder()
|
||||
* .add(Date.class, new Rfc3339DateJsonAdapter())
|
||||
* .build();
|
||||
* }</pre>
|
||||
*/
|
||||
public final class Rfc3339DateJsonAdapter extends JsonAdapter<Date> {
|
||||
@Override public synchronized Date fromJson(JsonReader reader) throws IOException {
|
||||
String string = reader.nextString();
|
||||
return Iso8601Utils.parse(string);
|
||||
}
|
||||
|
||||
@Override public synchronized void toJson(JsonWriter writer, Date value) throws IOException {
|
||||
String string = Iso8601Utils.format(value);
|
||||
writer.value(string);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.adapters;
|
||||
|
||||
import com.squareup.moshi.Json;
|
||||
import com.squareup.moshi.JsonDataException;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import okio.Buffer;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
@SuppressWarnings("CheckReturnValue")
|
||||
public final class EnumJsonAdapterTest {
|
||||
@Test public void toAndFromJson() throws Exception {
|
||||
EnumJsonAdapter<Roshambo> adapter = EnumJsonAdapter.create(Roshambo.class);
|
||||
assertThat(adapter.fromJson("\"ROCK\"")).isEqualTo(Roshambo.ROCK);
|
||||
assertThat(adapter.toJson(Roshambo.PAPER)).isEqualTo("\"PAPER\"");
|
||||
}
|
||||
|
||||
@Test public void withJsonName() throws Exception {
|
||||
EnumJsonAdapter<Roshambo> adapter = EnumJsonAdapter.create(Roshambo.class);
|
||||
assertThat(adapter.fromJson("\"scr\"")).isEqualTo(Roshambo.SCISSORS);
|
||||
assertThat(adapter.toJson(Roshambo.SCISSORS)).isEqualTo("\"scr\"");
|
||||
}
|
||||
|
||||
@Test public void withoutFallbackValue() throws Exception {
|
||||
EnumJsonAdapter<Roshambo> adapter = EnumJsonAdapter.create(Roshambo.class);
|
||||
JsonReader reader = JsonReader.of(new Buffer().writeUtf8("\"SPOCK\""));
|
||||
try {
|
||||
adapter.fromJson(reader);
|
||||
fail();
|
||||
} catch (JsonDataException expected) {
|
||||
assertThat(expected).hasMessage(
|
||||
"Expected one of [ROCK, PAPER, scr] but was SPOCK at path $");
|
||||
}
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||
}
|
||||
|
||||
@Test public void withFallbackValue() throws Exception {
|
||||
EnumJsonAdapter<Roshambo> adapter = EnumJsonAdapter.create(Roshambo.class)
|
||||
.withUnknownFallback(Roshambo.ROCK);
|
||||
JsonReader reader = JsonReader.of(new Buffer().writeUtf8("\"SPOCK\""));
|
||||
assertThat(adapter.fromJson(reader)).isEqualTo(Roshambo.ROCK);
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||
}
|
||||
|
||||
@Test public void withNullFallbackValue() throws Exception {
|
||||
EnumJsonAdapter<Roshambo> adapter = EnumJsonAdapter.create(Roshambo.class)
|
||||
.withUnknownFallback(null);
|
||||
JsonReader reader = JsonReader.of(new Buffer().writeUtf8("\"SPOCK\""));
|
||||
assertThat(adapter.fromJson(reader)).isNull();
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||
}
|
||||
|
||||
enum Roshambo {
|
||||
ROCK,
|
||||
PAPER,
|
||||
@Json(name = "scr") SCISSORS
|
||||
}
|
||||
}
|
|
@ -0,0 +1,300 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.adapters;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.JsonDataException;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.Moshi;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import okio.Buffer;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
@SuppressWarnings("CheckReturnValue")
|
||||
public final class PolymorphicJsonAdapterFactoryTest {
|
||||
@Test public void fromJson() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
assertThat(adapter.fromJson("{\"type\":\"success\",\"value\":\"Okay!\"}"))
|
||||
.isEqualTo(new Success("Okay!"));
|
||||
assertThat(adapter.fromJson("{\"type\":\"error\",\"error_logs\":{\"order\":66}}"))
|
||||
.isEqualTo(new Error(Collections.<String, Object>singletonMap("order", 66d)));
|
||||
}
|
||||
|
||||
@Test public void toJson() {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
assertThat(adapter.toJson(new Success("Okay!")))
|
||||
.isEqualTo("{\"type\":\"success\",\"value\":\"Okay!\"}");
|
||||
assertThat(adapter.toJson(new Error(Collections.<String, Object>singletonMap("order", 66))))
|
||||
.isEqualTo("{\"type\":\"error\",\"error_logs\":{\"order\":66}}");
|
||||
}
|
||||
|
||||
@Test public void unregisteredLabelValue() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
JsonReader reader =
|
||||
JsonReader.of(new Buffer().writeUtf8("{\"type\":\"data\",\"value\":\"Okay!\"}"));
|
||||
try {
|
||||
adapter.fromJson(reader);
|
||||
fail();
|
||||
} catch (JsonDataException expected) {
|
||||
assertThat(expected).hasMessage("Expected one of [success, error] for key 'type' but found"
|
||||
+ " 'data'. Register a subtype for this label.");
|
||||
}
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.BEGIN_OBJECT);
|
||||
}
|
||||
|
||||
@Test public void specifiedFallbackSubtype() throws IOException {
|
||||
Error fallbackError = new Error(Collections.<String, Object>emptyMap());
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error")
|
||||
.withDefaultValue(fallbackError))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
Message message = adapter.fromJson("{\"type\":\"data\",\"value\":\"Okay!\"}");
|
||||
assertThat(message).isSameAs(fallbackError);
|
||||
}
|
||||
|
||||
@Test public void specifiedNullFallbackSubtype() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error")
|
||||
.withDefaultValue(null))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
Message message = adapter.fromJson("{\"type\":\"data\",\"value\":\"Okay!\"}");
|
||||
assertThat(message).isNull();
|
||||
}
|
||||
|
||||
@Test public void unregisteredSubtype() {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
try {
|
||||
adapter.toJson(new EmptyMessage());
|
||||
} catch (IllegalArgumentException expected) {
|
||||
assertThat(expected).hasMessage("Expected one of [class"
|
||||
+ " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$Success, class"
|
||||
+ " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$Error] but found"
|
||||
+ " EmptyMessage, a class"
|
||||
+ " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$EmptyMessage. Register"
|
||||
+ " this subtype.");
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void nonStringLabelValue() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
try {
|
||||
adapter.fromJson("{\"type\":{},\"value\":\"Okay!\"}");
|
||||
fail();
|
||||
} catch (JsonDataException expected) {
|
||||
assertThat(expected).hasMessage("Expected a string but was BEGIN_OBJECT at path $.type");
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void nonObjectDoesNotConsume() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
JsonReader reader = JsonReader.of(new Buffer().writeUtf8("\"Failure\""));
|
||||
try {
|
||||
adapter.fromJson(reader);
|
||||
fail();
|
||||
} catch (JsonDataException expected) {
|
||||
assertThat(expected).hasMessage("Expected BEGIN_OBJECT but was STRING at path $");
|
||||
}
|
||||
assertThat(reader.nextString()).isEqualTo("Failure");
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||
}
|
||||
|
||||
@Test public void nonUniqueSubtypes() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Success.class, "data")
|
||||
.withSubtype(Error.class, "error"))
|
||||
.build();
|
||||
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
assertThat(adapter.fromJson("{\"type\":\"success\",\"value\":\"Okay!\"}"))
|
||||
.isEqualTo(new Success("Okay!"));
|
||||
assertThat(adapter.fromJson("{\"type\":\"data\",\"value\":\"Data!\"}"))
|
||||
.isEqualTo(new Success("Data!"));
|
||||
assertThat(adapter.fromJson("{\"type\":\"error\",\"error_logs\":{\"order\":66}}"))
|
||||
.isEqualTo(new Error(Collections.<String, Object>singletonMap("order", 66d)));
|
||||
}
|
||||
|
||||
@Test public void uniqueLabels() {
|
||||
PolymorphicJsonAdapterFactory<Message> factory =
|
||||
PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(Success.class, "data");
|
||||
try {
|
||||
factory.withSubtype(Error.class, "data");
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
assertThat(expected).hasMessage("Labels must be unique.");
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void nullSafe() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(Success.class, "success")
|
||||
.withSubtype(Error.class, "error"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
JsonReader reader = JsonReader.of(new Buffer().writeUtf8("null"));
|
||||
assertThat(adapter.fromJson(reader)).isNull();
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Longs that do not have an exact double representation are problematic for JSON. It is a bad
|
||||
* idea to use JSON for these values! But Moshi tries to retain long precision where possible.
|
||||
*/
|
||||
@Test public void unportableTypes() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(MessageWithUnportableTypes.class, "unportable"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
|
||||
|
||||
assertThat(adapter.toJson(new MessageWithUnportableTypes(9007199254740993L)))
|
||||
.isEqualTo("{\"type\":\"unportable\",\"long_value\":9007199254740993}");
|
||||
MessageWithUnportableTypes decoded = (MessageWithUnportableTypes) adapter.fromJson(
|
||||
"{\"type\":\"unportable\",\"long_value\":9007199254740993}");
|
||||
assertThat(decoded.long_value).isEqualTo(9007199254740993L);
|
||||
}
|
||||
|
||||
@Test public void failOnUnknownMissingTypeLabel() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
|
||||
.withSubtype(MessageWithType.class, "success"))
|
||||
.build();
|
||||
JsonAdapter<Message> adapter = moshi.adapter(Message.class).failOnUnknown();
|
||||
|
||||
MessageWithType decoded = (MessageWithType) adapter.fromJson(
|
||||
"{\"value\":\"Okay!\",\"type\":\"success\"}");
|
||||
assertThat(decoded.value).isEqualTo("Okay!");
|
||||
}
|
||||
|
||||
interface Message {
|
||||
}
|
||||
|
||||
static final class Success implements Message {
|
||||
final String value;
|
||||
|
||||
Success(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Success)) return false;
|
||||
Success success = (Success) o;
|
||||
return value.equals(success.value);
|
||||
}
|
||||
|
||||
@Override public int hashCode() {
|
||||
return value.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
static final class Error implements Message {
|
||||
final Map<String, Object> error_logs;
|
||||
|
||||
Error(Map<String, Object> error_logs) {
|
||||
this.error_logs = error_logs;
|
||||
}
|
||||
|
||||
@Override public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Error)) return false;
|
||||
Error error = (Error) o;
|
||||
return error_logs.equals(error.error_logs);
|
||||
}
|
||||
|
||||
@Override public int hashCode() {
|
||||
return error_logs.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
static final class EmptyMessage implements Message {
|
||||
@Override public String toString() {
|
||||
return "EmptyMessage";
|
||||
}
|
||||
}
|
||||
|
||||
static final class MessageWithUnportableTypes implements Message {
|
||||
final long long_value;
|
||||
|
||||
MessageWithUnportableTypes(long long_value) {
|
||||
this.long_value = long_value;
|
||||
}
|
||||
}
|
||||
|
||||
static final class MessageWithType implements Message {
|
||||
final String type;
|
||||
final String value;
|
||||
|
||||
MessageWithType(String type, String value) {
|
||||
this.type = type;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,8 +13,9 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi;
|
||||
package com.squareup.moshi.adapters;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
|
@ -1,9 +1,10 @@
|
|||
<?xml version="1.0"?>
|
||||
<!DOCTYPE module PUBLIC
|
||||
"-//Puppy Crawl//DTD Check Configuration 1.2//EN"
|
||||
"http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
|
||||
"-//Puppy Crawl//DTD Check Configuration 1.3//EN"
|
||||
"http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
|
||||
|
||||
<module name="Checker">
|
||||
<module name="SuppressWarningsFilter"/>
|
||||
<module name="NewlineAtEndOfFile"/>
|
||||
<module name="FileLength"/>
|
||||
<module name="FileTabCharacter"/>
|
||||
|
@ -44,7 +45,7 @@
|
|||
<module name="LocalVariableName"/>
|
||||
<module name="MemberName"/>
|
||||
<module name="MethodName"/>
|
||||
<module name="PackageName"/>
|
||||
<!--<module name="PackageName"/>-->
|
||||
<module name="ParameterName"/>
|
||||
<module name="StaticVariableName"/>
|
||||
<module name="TypeName"/>
|
||||
|
@ -56,7 +57,9 @@
|
|||
<module name="IllegalImport"/>
|
||||
<!-- defaults to sun.* packages -->
|
||||
<module name="RedundantImport"/>
|
||||
<module name="UnusedImports"/>
|
||||
<module name="UnusedImports">
|
||||
<property name="processJavadoc" value="true"/>
|
||||
</module>
|
||||
|
||||
|
||||
<!-- Checks for Size Violations. -->
|
||||
|
@ -64,7 +67,9 @@
|
|||
<module name="LineLength">
|
||||
<property name="max" value="100"/>
|
||||
</module>
|
||||
<module name="MethodLength"/>
|
||||
<module name="MethodLength">
|
||||
<property name="max" value="200"/>
|
||||
</module>
|
||||
|
||||
|
||||
<!-- Checks for whitespace -->
|
||||
|
@ -78,7 +83,15 @@
|
|||
<module name="ParenPad"/>
|
||||
<module name="TypecastParenPad"/>
|
||||
<module name="WhitespaceAfter"/>
|
||||
<module name="WhitespaceAround"/>
|
||||
<module name="WhitespaceAround">
|
||||
<property name="tokens"
|
||||
value="ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR, BXOR_ASSIGN,
|
||||
COLON, DIV, DIV_ASSIGN, DO_WHILE, EQUAL, GE, GT, LAND, LCURLY, LE, LITERAL_CATCH,
|
||||
LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF, LITERAL_RETURN,
|
||||
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS,
|
||||
MINUS_ASSIGN, MOD, MOD_ASSIGN, NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, SL, SLIST,
|
||||
SL_ASSIGN, SR, SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT, TYPE_EXTENSION_AND"/>
|
||||
</module>
|
||||
|
||||
|
||||
<!-- Modifier Checks -->
|
||||
|
@ -108,7 +121,7 @@
|
|||
<!--module name="InnerAssignment"/-->
|
||||
<!--module name="MagicNumber"/-->
|
||||
<!--module name="MissingSwitchDefault"/-->
|
||||
<module name="RedundantThrows"/>
|
||||
<!--<module name="RedundantThrows"/>-->
|
||||
<module name="SimplifyBooleanExpression"/>
|
||||
<module name="SimplifyBooleanReturn"/>
|
||||
|
||||
|
@ -127,5 +140,8 @@
|
|||
<!--module name="FinalParameters"/-->
|
||||
<!--module name="TodoComment"/-->
|
||||
<module name="UpperEll"/>
|
||||
|
||||
<!-- Make the @SuppressWarnings annotations available to Checkstyle -->
|
||||
<module name="SuppressWarningsHolder"/>
|
||||
</module>
|
||||
</module>
|
||||
|
|
|
@ -6,12 +6,17 @@
|
|||
<parent>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi-parent</artifactId>
|
||||
<version>1.4.0</version>
|
||||
<version>1.9.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>moshi-examples</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.google.code.findbugs</groupId>
|
||||
<artifactId>jsr305</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi</artifactId>
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.recipes;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.JsonWriter;
|
||||
import com.squareup.moshi.Moshi;
|
||||
import java.io.IOException;
|
||||
import okio.ByteString;
|
||||
|
||||
public final class ByteStrings {
|
||||
public void run() throws Exception {
|
||||
String json = "\"TW9zaGksIE9saXZlLCBXaGl0ZSBDaGluPw\"";
|
||||
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(ByteString.class, new Base64ByteStringAdapter())
|
||||
.build();
|
||||
JsonAdapter<ByteString> jsonAdapter = moshi.adapter(ByteString.class);
|
||||
|
||||
ByteString byteString = jsonAdapter.fromJson(json);
|
||||
System.out.println(byteString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats byte strings using <a href="http://www.ietf.org/rfc/rfc2045.txt">Base64</a>. No line
|
||||
* breaks or whitespace is included in the encoded form.
|
||||
*/
|
||||
public final class Base64ByteStringAdapter extends JsonAdapter<ByteString> {
|
||||
@Override public ByteString fromJson(JsonReader reader) throws IOException {
|
||||
String base64 = reader.nextString();
|
||||
return ByteString.decodeBase64(base64);
|
||||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, ByteString value) throws IOException {
|
||||
String string = value.base64();
|
||||
writer.value(string);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
new ByteStrings().run();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.recipes;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.JsonWriter;
|
||||
import com.squareup.moshi.Moshi;
|
||||
import com.squareup.moshi.Types;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Set;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public final class CustomAdapterFactory {
|
||||
public void run() throws Exception {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(new SortedSetAdapterFactory())
|
||||
.build();
|
||||
JsonAdapter<SortedSet<String>> jsonAdapter = moshi.adapter(
|
||||
Types.newParameterizedType(SortedSet.class, String.class));
|
||||
|
||||
TreeSet<String> model = new TreeSet<>();
|
||||
model.add("a");
|
||||
model.add("b");
|
||||
model.add("c");
|
||||
|
||||
String json = jsonAdapter.toJson(model);
|
||||
System.out.println(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* This class composes an adapter for any element type into an adapter for a sorted set of those
|
||||
* elements. For example, given a {@code JsonAdapter<MovieTicket>}, use this to get a
|
||||
* {@code JsonAdapter<SortedSet<MovieTicket>>}. It works by looping over the input elements when
|
||||
* both reading and writing.
|
||||
*/
|
||||
static final class SortedSetAdapter<T> extends JsonAdapter<SortedSet<T>> {
|
||||
private final JsonAdapter<T> elementAdapter;
|
||||
|
||||
SortedSetAdapter(JsonAdapter<T> elementAdapter) {
|
||||
this.elementAdapter = elementAdapter;
|
||||
}
|
||||
|
||||
@Override public SortedSet<T> fromJson(JsonReader reader) throws IOException {
|
||||
TreeSet<T> result = new TreeSet<>();
|
||||
reader.beginArray();
|
||||
while (reader.hasNext()) {
|
||||
result.add(elementAdapter.fromJson(reader));
|
||||
}
|
||||
reader.endArray();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, SortedSet<T> set) throws IOException {
|
||||
writer.beginArray();
|
||||
for (T element : set) {
|
||||
elementAdapter.toJson(writer, element);
|
||||
}
|
||||
writer.endArray();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moshi asks this class to create JSON adapters. It only knows how to create JSON adapters for
|
||||
* {@code SortedSet} types, so it returns null for all other requests. When it does get a request
|
||||
* for a {@code SortedSet<X>}, it asks Moshi for an adapter of the element type {@code X} and then
|
||||
* uses that to create an adapter for the set.
|
||||
*/
|
||||
static class SortedSetAdapterFactory implements JsonAdapter.Factory {
|
||||
@Override public @Nullable JsonAdapter<?> create(
|
||||
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
if (!annotations.isEmpty()) {
|
||||
return null; // Annotations? This factory doesn't apply.
|
||||
}
|
||||
|
||||
if (!(type instanceof ParameterizedType)) {
|
||||
return null; // No type parameter? This factory doesn't apply.
|
||||
}
|
||||
|
||||
ParameterizedType parameterizedType = (ParameterizedType) type;
|
||||
if (parameterizedType.getRawType() != SortedSet.class) {
|
||||
return null; // Not a sorted set? This factory doesn't apply.
|
||||
}
|
||||
|
||||
Type elementType = parameterizedType.getActualTypeArguments()[0];
|
||||
JsonAdapter<Object> elementAdapter = moshi.adapter(elementType);
|
||||
|
||||
return new SortedSetAdapter<>(elementAdapter).nullSafe();
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
new CustomAdapterFactory().run();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright (C) 2015 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.recipes;
|
||||
|
||||
|
||||
import com.squareup.moshi.FromJson;
|
||||
import com.squareup.moshi.Json;
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.Moshi;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
|
||||
public final class CustomAdapterWithDelegate {
|
||||
public void run() throws Exception {
|
||||
// We want to match any Stage that starts with 'in-progress' as Stage.IN_PROGRESS
|
||||
// and leave the rest of the enum values as to match as normal.
|
||||
Moshi moshi = new Moshi.Builder().add(new StageAdapter()).build();
|
||||
JsonAdapter<Stage> jsonAdapter = moshi.adapter(Stage.class);
|
||||
|
||||
System.out.println(jsonAdapter.fromJson("\"not-started\""));
|
||||
System.out.println(jsonAdapter.fromJson("\"in-progress\""));
|
||||
System.out.println(jsonAdapter.fromJson("\"in-progress-step1\""));
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
new CustomAdapterWithDelegate().run();
|
||||
}
|
||||
|
||||
private enum Stage {
|
||||
@Json(name = "not-started") NOT_STARTED,
|
||||
@Json(name = "in-progress") IN_PROGRESS,
|
||||
@Json(name = "rejected") REJECTED,
|
||||
@Json(name = "completed") COMPLETED
|
||||
}
|
||||
|
||||
private static final class StageAdapter {
|
||||
@FromJson
|
||||
@Nullable
|
||||
Stage fromJson(JsonReader jsonReader, JsonAdapter<Stage> delegate) throws IOException {
|
||||
String value = jsonReader.nextString();
|
||||
|
||||
Stage stage;
|
||||
if (value.startsWith("in-progress")) {
|
||||
stage = Stage.IN_PROGRESS;
|
||||
} else {
|
||||
stage = delegate.fromJsonValue(value);
|
||||
}
|
||||
return stage;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ import com.squareup.moshi.recipes.models.Player;
|
|||
|
||||
public final class CustomFieldName {
|
||||
public void run() throws Exception {
|
||||
String json = ""
|
||||
String json = ""
|
||||
+ "{"
|
||||
+ " \"username\": \"jesse\","
|
||||
+ " \"lucky number\": 32"
|
||||
|
|
|
@ -24,6 +24,7 @@ import java.io.IOException;
|
|||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public final class DefaultOnDataMismatchAdapter<T> extends JsonAdapter<T> {
|
||||
private final JsonAdapter<T> delegate;
|
||||
|
@ -35,17 +36,20 @@ public final class DefaultOnDataMismatchAdapter<T> extends JsonAdapter<T> {
|
|||
}
|
||||
|
||||
@Override public T fromJson(JsonReader reader) throws IOException {
|
||||
// Read the value first so that the reader will be in a known state even if there's an
|
||||
// exception. Otherwise it may be awkward to recover: it might be between calls to
|
||||
// beginObject() and endObject() for example.
|
||||
Object jsonValue = reader.readJsonValue();
|
||||
|
||||
// Use the delegate to convert the JSON value to the target type.
|
||||
// Use a peeked reader to leave the reader in a known state even if there's an exception.
|
||||
JsonReader peeked = reader.peekJson();
|
||||
T result;
|
||||
try {
|
||||
return delegate.fromJsonValue(jsonValue);
|
||||
// Attempt to decode to the target type with the peeked reader.
|
||||
result = delegate.fromJson(peeked);
|
||||
} catch (JsonDataException e) {
|
||||
return defaultValue;
|
||||
result = defaultValue;
|
||||
} finally {
|
||||
peeked.close();
|
||||
}
|
||||
// Skip the value back on the reader, no matter the state of the peeked reader.
|
||||
reader.skipValue();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, T value) throws IOException {
|
||||
|
@ -54,7 +58,7 @@ public final class DefaultOnDataMismatchAdapter<T> extends JsonAdapter<T> {
|
|||
|
||||
public static <T> Factory newFactory(final Class<T> type, final T defaultValue) {
|
||||
return new Factory() {
|
||||
@Override public JsonAdapter<?> create(
|
||||
@Override public @Nullable JsonAdapter<?> create(
|
||||
Type requestedType, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
if (type != requestedType) return null;
|
||||
JsonAdapter<T> delegate = moshi.nextAdapter(this, type, annotations);
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.recipes;
|
||||
|
||||
import com.squareup.moshi.Json;
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.JsonQualifier;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.JsonWriter;
|
||||
import com.squareup.moshi.Moshi;
|
||||
import com.squareup.moshi.Types;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
final class FallbackEnum {
|
||||
@Retention(RUNTIME)
|
||||
@JsonQualifier
|
||||
public @interface Fallback {
|
||||
/**
|
||||
* The enum name.
|
||||
*/
|
||||
String value();
|
||||
}
|
||||
|
||||
public static final class FallbackEnumJsonAdapter<T extends Enum<T>> extends JsonAdapter<T> {
|
||||
public static final Factory FACTORY = new Factory() {
|
||||
@Nullable @Override @SuppressWarnings("unchecked")
|
||||
public JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
Class<?> rawType = Types.getRawType(type);
|
||||
if (!rawType.isEnum()) {
|
||||
return null;
|
||||
}
|
||||
if (annotations.size() != 1) {
|
||||
return null;
|
||||
}
|
||||
Annotation annotation = annotations.iterator().next();
|
||||
if (!(annotation instanceof Fallback)) {
|
||||
return null;
|
||||
}
|
||||
Class<Enum> enumType = (Class<Enum>) rawType;
|
||||
Enum<?> fallback = Enum.valueOf(enumType, ((Fallback) annotation).value());
|
||||
return new FallbackEnumJsonAdapter<>(enumType, fallback);
|
||||
}
|
||||
};
|
||||
|
||||
final Class<T> enumType;
|
||||
final String[] nameStrings;
|
||||
final T[] constants;
|
||||
final JsonReader.Options options;
|
||||
final T defaultValue;
|
||||
|
||||
FallbackEnumJsonAdapter(Class<T> enumType, T defaultValue) {
|
||||
this.enumType = enumType;
|
||||
this.defaultValue = defaultValue;
|
||||
try {
|
||||
constants = enumType.getEnumConstants();
|
||||
nameStrings = new String[constants.length];
|
||||
for (int i = 0; i < constants.length; i++) {
|
||||
T constant = constants[i];
|
||||
Json annotation = enumType.getField(constant.name()).getAnnotation(Json.class);
|
||||
String name = annotation != null ? annotation.name() : constant.name();
|
||||
nameStrings[i] = name;
|
||||
}
|
||||
options = JsonReader.Options.of(nameStrings);
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public T fromJson(JsonReader reader) throws IOException {
|
||||
int index = reader.selectString(options);
|
||||
if (index != -1) return constants[index];
|
||||
reader.nextString();
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, T value) throws IOException {
|
||||
writer.value(nameStrings[value.ordinal()]);
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
return "JsonAdapter(" + enumType.getName() + ").defaultValue( " + defaultValue + ")";
|
||||
}
|
||||
}
|
||||
|
||||
static final class Example {
|
||||
enum Transportation {
|
||||
WALKING, BIKING, TRAINS, PLANES
|
||||
}
|
||||
|
||||
@Fallback("WALKING") final Transportation transportation;
|
||||
|
||||
Example(Transportation transportation) {
|
||||
this.transportation = transportation;
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
return transportation.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(FallbackEnumJsonAdapter.FACTORY)
|
||||
.build();
|
||||
JsonAdapter<Example> adapter = moshi.adapter(Example.class);
|
||||
System.out.println(adapter.fromJson("{\"transportation\":\"CARS\"}"));
|
||||
}
|
||||
|
||||
private FallbackEnum() {
|
||||
}
|
||||
}
|
|
@ -44,6 +44,7 @@ public final class FromJsonWithoutStrings {
|
|||
new FromJsonWithoutStrings().run();
|
||||
}
|
||||
|
||||
@SuppressWarnings("checkstyle:membername")
|
||||
private static final class EventJson {
|
||||
String title;
|
||||
String begin_date;
|
||||
|
@ -55,10 +56,10 @@ public final class FromJsonWithoutStrings {
|
|||
String beginDateAndTime;
|
||||
|
||||
@Override public String toString() {
|
||||
return "Event{" +
|
||||
"title='" + title + '\'' +
|
||||
", beginDateAndTime='" + beginDateAndTime + '\'' +
|
||||
'}';
|
||||
return "Event{"
|
||||
+ "title='" + title + '\''
|
||||
+ ", beginDateAndTime='" + beginDateAndTime + '\''
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.recipes;
|
||||
|
||||
import com.squareup.moshi.FromJson;
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.JsonDataException;
|
||||
import com.squareup.moshi.JsonQualifier;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.JsonWriter;
|
||||
import com.squareup.moshi.Moshi;
|
||||
import com.squareup.moshi.ToJson;
|
||||
import com.squareup.moshi.recipes.models.Card;
|
||||
import com.squareup.moshi.recipes.models.Suit;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
public final class MultipleFormats {
|
||||
public void run() throws Exception {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(new MultipleFormatsCardAdapter())
|
||||
.add(new CardStringAdapter())
|
||||
.build();
|
||||
|
||||
JsonAdapter<Card> cardAdapter = moshi.adapter(Card.class);
|
||||
|
||||
// Decode cards from one format or the other.
|
||||
System.out.println(cardAdapter.fromJson("\"5D\""));
|
||||
System.out.println(cardAdapter.fromJson("{\"suit\": \"SPADES\", \"rank\": 5}"));
|
||||
|
||||
// Cards are always encoded as strings.
|
||||
System.out.println(cardAdapter.toJson(new Card('5', Suit.CLUBS)));
|
||||
}
|
||||
|
||||
/** Handles cards either as strings "5D" or as objects {"suit": "SPADES", "rank": 5}. */
|
||||
public final class MultipleFormatsCardAdapter {
|
||||
@ToJson void toJson(JsonWriter writer, Card value,
|
||||
@CardString JsonAdapter<Card> stringAdapter) throws IOException {
|
||||
stringAdapter.toJson(writer, value);
|
||||
}
|
||||
|
||||
@FromJson Card fromJson(JsonReader reader, @CardString JsonAdapter<Card> stringAdapter,
|
||||
JsonAdapter<Card> defaultAdapter) throws IOException {
|
||||
if (reader.peek() == JsonReader.Token.STRING) {
|
||||
return stringAdapter.fromJson(reader);
|
||||
} else {
|
||||
return defaultAdapter.fromJson(reader);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Handles cards as strings only. */
|
||||
public final class CardStringAdapter {
|
||||
@ToJson String toJson(@CardString Card card) {
|
||||
return card.rank + card.suit.name().substring(0, 1);
|
||||
}
|
||||
|
||||
@FromJson @CardString Card fromJson(String card) {
|
||||
if (card.length() != 2) throw new JsonDataException("Unknown card: " + card);
|
||||
|
||||
char rank = card.charAt(0);
|
||||
switch (card.charAt(1)) {
|
||||
case 'C': return new Card(rank, Suit.CLUBS);
|
||||
case 'D': return new Card(rank, Suit.DIAMONDS);
|
||||
case 'H': return new Card(rank, Suit.HEARTS);
|
||||
case 'S': return new Card(rank, Suit.SPADES);
|
||||
default: throw new JsonDataException("unknown suit: " + card);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@JsonQualifier
|
||||
@interface CardString {
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
new MultipleFormats().run();
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@ package com.squareup.moshi.recipes;
|
|||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.Moshi;
|
||||
import com.squareup.moshi.Rfc3339DateJsonAdapter;
|
||||
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter;
|
||||
import com.squareup.moshi.recipes.models.Tournament;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
|
|
|
@ -21,7 +21,7 @@ import com.squareup.moshi.recipes.models.BlackjackHand;
|
|||
|
||||
public final class ReadJson {
|
||||
public void run() throws Exception {
|
||||
String json = ""
|
||||
String json = ""
|
||||
+ "{\n"
|
||||
+ " \"hidden_card\": {\n"
|
||||
+ " \"rank\": \"6\",\n"
|
||||
|
|
|
@ -28,10 +28,14 @@ import java.lang.annotation.Annotation;
|
|||
import java.lang.annotation.Retention;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
final class Unwrap {
|
||||
private Unwrap() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
String json = ""
|
||||
+ "{\"data\":"
|
||||
|
@ -48,8 +52,8 @@ final class Unwrap {
|
|||
|
||||
public static final class EnvelopeJsonAdapter extends JsonAdapter<Object> {
|
||||
public static final JsonAdapter.Factory FACTORY = new Factory() {
|
||||
@Override
|
||||
public JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
@Override public @Nullable JsonAdapter<?> create(
|
||||
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
Set<? extends Annotation> delegateAnnotations =
|
||||
Types.nextAnnotations(annotations, Enveloped.class);
|
||||
if (delegateAnnotations == null) {
|
||||
|
@ -84,7 +88,7 @@ final class Unwrap {
|
|||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, Object value) throws IOException {
|
||||
delegate.toJson(new Envelope<>(value));
|
||||
delegate.toJson(writer, new Envelope<>(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,13 +17,14 @@ package com.squareup.moshi.recipes.models;
|
|||
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings("checkstyle:membername")
|
||||
public final class BlackjackHand {
|
||||
public final Card hidden_card;
|
||||
public final List<Card> visible_cards;
|
||||
|
||||
public BlackjackHand(Card hidden_card, List<Card> visible_cards) {
|
||||
this.hidden_card = hidden_card;
|
||||
this.visible_cards = visible_cards;
|
||||
public BlackjackHand(Card hiddenCard, List<Card> visibleCards) {
|
||||
this.hidden_card = hiddenCard;
|
||||
this.visible_cards = visibleCards;
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
/** Moshi code samples. */
|
||||
@javax.annotation.ParametersAreNonnullByDefault
|
||||
package com.squareup.moshi.recipes;
|
205
kotlin/codegen/pom.xml
Normal file
205
kotlin/codegen/pom.xml
Normal file
|
@ -0,0 +1,205 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi-parent</artifactId>
|
||||
<version>1.9.0-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>moshi-kotlin-codegen</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup</groupId>
|
||||
<artifactId>kotlinpoet</artifactId>
|
||||
<version>1.2.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.ltgt.gradle.incap</groupId>
|
||||
<artifactId>incap</artifactId>
|
||||
<version>${incap.version}</version>
|
||||
<scope>provided</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.auto</groupId>
|
||||
<artifactId>auto-common</artifactId>
|
||||
<version>0.10</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.auto.service</groupId>
|
||||
<artifactId>auto-service-annotations</artifactId>
|
||||
<version>${autoservice.version}</version>
|
||||
<scope>provided</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!--
|
||||
The Kotlin compiler must be near the end of the list because its .jar file includes an
|
||||
obsolete version of Guava!
|
||||
-->
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-compiler-embeddable</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-annotation-processing-embeddable</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>me.eugeniomarletti.kotlin.metadata</groupId>
|
||||
<artifactId>kotlin-metadata</artifactId>
|
||||
</dependency>
|
||||
<!--
|
||||
Though we don't use compile-testing, including it is a convenient way to get tools.jar on the
|
||||
classpath. This dependency is required by kapt3.
|
||||
-->
|
||||
<dependency>
|
||||
<groupId>com.google.testing.compile</groupId>
|
||||
<artifactId>compile-testing</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<properties>
|
||||
<maven.javadoc.skip>true</maven.javadoc.skip><!-- We use Dokka instead. -->
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>kapt</id>
|
||||
<goals>
|
||||
<goal>kapt</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<sourceDirs>
|
||||
<sourceDir>src/main/kotlin</sourceDir>
|
||||
<sourceDir>src/main/java</sourceDir>
|
||||
</sourceDirs>
|
||||
<annotationProcessorPaths>
|
||||
<annotationProcessorPath>
|
||||
<groupId>com.google.auto.service</groupId>
|
||||
<artifactId>auto-service</artifactId>
|
||||
<version>${autoservice.version}</version>
|
||||
</annotationProcessorPath>
|
||||
<annotationProcessorPath>
|
||||
<groupId>net.ltgt.gradle.incap</groupId>
|
||||
<artifactId>incap-processor</artifactId>
|
||||
<version>${incap.version}</version>
|
||||
</annotationProcessorPath>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>test-compile</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals>
|
||||
<goal>test-compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>testCompile</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals>
|
||||
<goal>testCompile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>2.21.0</version>
|
||||
<configuration>
|
||||
<!--
|
||||
Suppress the surefire classloader which prevents introspecting the classpath.
|
||||
http://maven.apache.org/surefire/maven-surefire-plugin/examples/class-loading.html
|
||||
-->
|
||||
<useManifestOnlyJar>false</useManifestOnlyJar>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>${maven-assembly.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<descriptors>
|
||||
<descriptor>src/assembly/dokka.xml</descriptor>
|
||||
</descriptors>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.dokka</groupId>
|
||||
<artifactId>dokka-maven-plugin</artifactId>
|
||||
<version>${dokka.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>dokka</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
16
kotlin/codegen/src/assembly/dokka.xml
Normal file
16
kotlin/codegen/src/assembly/dokka.xml
Normal file
|
@ -0,0 +1,16 @@
|
|||
<assembly
|
||||
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
|
||||
<id>javadoc</id>
|
||||
<formats>
|
||||
<format>jar</format>
|
||||
</formats>
|
||||
<baseDirectory>/</baseDirectory>
|
||||
<fileSets>
|
||||
<fileSet>
|
||||
<directory>target/dokka/moshi-kotlin-codegen</directory>
|
||||
<outputDirectory>/</outputDirectory>
|
||||
</fileSet>
|
||||
</fileSets>
|
||||
</assembly>
|
|
@ -0,0 +1,327 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.kotlin.codegen
|
||||
|
||||
import com.squareup.kotlinpoet.ARRAY
|
||||
import com.squareup.kotlinpoet.AnnotationSpec
|
||||
import com.squareup.kotlinpoet.CodeBlock
|
||||
import com.squareup.kotlinpoet.FileSpec
|
||||
import com.squareup.kotlinpoet.FunSpec
|
||||
import com.squareup.kotlinpoet.KModifier
|
||||
import com.squareup.kotlinpoet.NameAllocator
|
||||
import com.squareup.kotlinpoet.ParameterSpec
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
|
||||
import com.squareup.kotlinpoet.PropertySpec
|
||||
import com.squareup.kotlinpoet.TypeSpec
|
||||
import com.squareup.kotlinpoet.TypeVariableName
|
||||
import com.squareup.kotlinpoet.asClassName
|
||||
import com.squareup.kotlinpoet.asTypeName
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import com.squareup.moshi.Moshi
|
||||
import me.eugeniomarletti.kotlin.metadata.isDataClass
|
||||
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility
|
||||
import me.eugeniomarletti.kotlin.metadata.visibility
|
||||
import java.lang.reflect.Type
|
||||
import javax.lang.model.element.TypeElement
|
||||
|
||||
/** Generates a JSON adapter for a target type. */
|
||||
internal class AdapterGenerator(
|
||||
target: TargetType,
|
||||
private val propertyList: List<PropertyGenerator>
|
||||
) {
|
||||
private val className = target.name
|
||||
private val isDataClass = target.proto.isDataClass
|
||||
private val visibility = target.proto.visibility!!
|
||||
private val typeVariables = target.typeVariables
|
||||
|
||||
private val nameAllocator = NameAllocator()
|
||||
private val adapterName = "${className.simpleNames.joinToString(separator = "_")}JsonAdapter"
|
||||
private val originalElement = target.element
|
||||
private val originalTypeName = target.element.asType().asTypeName()
|
||||
|
||||
private val moshiParam = ParameterSpec.builder(
|
||||
nameAllocator.newName("moshi"),
|
||||
Moshi::class).build()
|
||||
private val typesParam = ParameterSpec.builder(
|
||||
nameAllocator.newName("types"),
|
||||
ARRAY.parameterizedBy(Type::class.asTypeName()))
|
||||
.build()
|
||||
private val readerParam = ParameterSpec.builder(
|
||||
nameAllocator.newName("reader"),
|
||||
JsonReader::class)
|
||||
.build()
|
||||
private val writerParam = ParameterSpec.builder(
|
||||
nameAllocator.newName("writer"),
|
||||
JsonWriter::class)
|
||||
.build()
|
||||
private val valueParam = ParameterSpec.builder(
|
||||
nameAllocator.newName("value"),
|
||||
originalTypeName.copy(nullable = true))
|
||||
.build()
|
||||
private val jsonAdapterTypeName = JsonAdapter::class.asClassName().parameterizedBy(originalTypeName)
|
||||
|
||||
// selectName() API setup
|
||||
private val optionsProperty = PropertySpec.builder(
|
||||
nameAllocator.newName("options"), JsonReader.Options::class.asTypeName(),
|
||||
KModifier.PRIVATE)
|
||||
.initializer("%T.of(${propertyList.joinToString(", ") {
|
||||
CodeBlock.of("%S", it.jsonName).toString()
|
||||
}})", JsonReader.Options::class.asTypeName())
|
||||
.build()
|
||||
|
||||
fun generateFile(generatedOption: TypeElement?): FileSpec {
|
||||
for (property in propertyList) {
|
||||
property.allocateNames(nameAllocator)
|
||||
}
|
||||
|
||||
val result = FileSpec.builder(className.packageName, adapterName)
|
||||
result.addComment("Code generated by moshi-kotlin-codegen. Do not edit.")
|
||||
result.addType(generateType(generatedOption))
|
||||
return result.build()
|
||||
}
|
||||
|
||||
private fun generateType(generatedOption: TypeElement?): TypeSpec {
|
||||
val result = TypeSpec.classBuilder(adapterName)
|
||||
.addOriginatingElement(originalElement)
|
||||
|
||||
generatedOption?.let {
|
||||
result.addAnnotation(AnnotationSpec.builder(it.asClassName())
|
||||
.addMember("value = [%S]", JsonClassCodegenProcessor::class.java.canonicalName)
|
||||
.addMember("comments = %S", "https://github.com/square/moshi")
|
||||
.build())
|
||||
}
|
||||
|
||||
result.superclass(jsonAdapterTypeName)
|
||||
|
||||
if (typeVariables.isNotEmpty()) {
|
||||
result.addTypeVariables(typeVariables)
|
||||
}
|
||||
|
||||
// TODO make this configurable. Right now it just matches the source model
|
||||
if (visibility == Visibility.INTERNAL) {
|
||||
result.addModifiers(KModifier.INTERNAL)
|
||||
}
|
||||
|
||||
result.primaryConstructor(generateConstructor())
|
||||
|
||||
val typeRenderer: TypeRenderer = object : TypeRenderer() {
|
||||
override fun renderTypeVariable(typeVariable: TypeVariableName): CodeBlock {
|
||||
val index = typeVariables.indexOfFirst { it == typeVariable }
|
||||
check(index != -1) { "Unexpected type variable $typeVariable" }
|
||||
return CodeBlock.of("%N[%L]", typesParam, index)
|
||||
}
|
||||
}
|
||||
|
||||
result.addProperty(optionsProperty)
|
||||
for (uniqueAdapter in propertyList.distinctBy { it.delegateKey }) {
|
||||
result.addProperty(uniqueAdapter.delegateKey.generateProperty(
|
||||
nameAllocator, typeRenderer, moshiParam, uniqueAdapter.name))
|
||||
}
|
||||
|
||||
result.addFunction(generateToStringFun())
|
||||
result.addFunction(generateFromJsonFun())
|
||||
result.addFunction(generateToJsonFun())
|
||||
|
||||
return result.build()
|
||||
}
|
||||
|
||||
private fun generateConstructor(): FunSpec {
|
||||
val result = FunSpec.constructorBuilder()
|
||||
result.addParameter(moshiParam)
|
||||
|
||||
if (typeVariables.isNotEmpty()) {
|
||||
result.addParameter(typesParam)
|
||||
}
|
||||
|
||||
return result.build()
|
||||
}
|
||||
|
||||
private fun generateToStringFun(): FunSpec {
|
||||
return FunSpec.builder("toString")
|
||||
.addModifiers(KModifier.OVERRIDE)
|
||||
.returns(String::class)
|
||||
.addStatement("return %S",
|
||||
"GeneratedJsonAdapter(${originalTypeName.rawType().simpleNames.joinToString(".")})")
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun jsonDataException(
|
||||
description: String,
|
||||
identifier: String,
|
||||
condition: String,
|
||||
reader: ParameterSpec
|
||||
): CodeBlock {
|
||||
return CodeBlock.of("%T(%T(%S).append(%S).append(%S).append(%N.path).toString())",
|
||||
JsonDataException::class, StringBuilder::class, description, identifier, condition, reader)
|
||||
}
|
||||
|
||||
private fun generateFromJsonFun(): FunSpec {
|
||||
val resultName = nameAllocator.newName("result")
|
||||
|
||||
val result = FunSpec.builder("fromJson")
|
||||
.addModifiers(KModifier.OVERRIDE)
|
||||
.addParameter(readerParam)
|
||||
.returns(originalTypeName)
|
||||
|
||||
for (property in propertyList) {
|
||||
result.addCode("%L", property.generateLocalProperty())
|
||||
if (property.differentiateAbsentFromNull) {
|
||||
result.addCode("%L", property.generateLocalIsPresentProperty())
|
||||
}
|
||||
}
|
||||
|
||||
result.addStatement("%N.beginObject()", readerParam)
|
||||
result.beginControlFlow("while (%N.hasNext())", readerParam)
|
||||
result.beginControlFlow("when (%N.selectName(%N))", readerParam, optionsProperty)
|
||||
|
||||
propertyList.forEachIndexed { index, property ->
|
||||
if (property.differentiateAbsentFromNull) {
|
||||
result.beginControlFlow("%L -> ", index)
|
||||
if (property.delegateKey.nullable) {
|
||||
result.addStatement("%N = %N.fromJson(%N)",
|
||||
property.localName, nameAllocator[property.delegateKey], readerParam)
|
||||
} else {
|
||||
val exception = jsonDataException(
|
||||
"Non-null value '", property.localName, "' was null at ", readerParam)
|
||||
result.addStatement("%N = %N.fromJson(%N) ?: throw·%L",
|
||||
property.localName, nameAllocator[property.delegateKey], readerParam, exception)
|
||||
}
|
||||
result.addStatement("%N = true", property.localIsPresentName)
|
||||
result.endControlFlow()
|
||||
} else {
|
||||
if (property.delegateKey.nullable) {
|
||||
result.addStatement("%L -> %N = %N.fromJson(%N)",
|
||||
index, property.localName, nameAllocator[property.delegateKey], readerParam)
|
||||
} else {
|
||||
val exception = jsonDataException(
|
||||
"Non-null value '", property.localName, "' was null at ", readerParam)
|
||||
result.addStatement("%L -> %N = %N.fromJson(%N) ?: throw·%L",
|
||||
index, property.localName, nameAllocator[property.delegateKey], readerParam,
|
||||
exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.beginControlFlow("-1 ->")
|
||||
result.addComment("Unknown name, skip it.")
|
||||
result.addStatement("%N.skipName()", readerParam)
|
||||
result.addStatement("%N.skipValue()", readerParam)
|
||||
result.endControlFlow()
|
||||
|
||||
result.endControlFlow() // when
|
||||
result.endControlFlow() // while
|
||||
result.addStatement("%N.endObject()", readerParam)
|
||||
|
||||
// Call the constructor providing only required parameters.
|
||||
var hasOptionalParameters = false
|
||||
result.addCode("«var %N = %T(", resultName, originalTypeName)
|
||||
var separator = "\n"
|
||||
for (property in propertyList) {
|
||||
if (!property.hasConstructorParameter) {
|
||||
continue
|
||||
}
|
||||
if (property.hasDefault) {
|
||||
hasOptionalParameters = true
|
||||
continue
|
||||
}
|
||||
result.addCode(separator)
|
||||
result.addCode("%N = %N", property.name, property.localName)
|
||||
if (property.isRequired) {
|
||||
result.addCode(" ?: throw·%L", jsonDataException(
|
||||
"Required property '", property.localName, "' missing at ", readerParam))
|
||||
}
|
||||
separator = ",\n"
|
||||
}
|
||||
result.addCode(")»\n", originalTypeName)
|
||||
|
||||
// Call either the constructor again, or the copy() method, this time providing any optional
|
||||
// parameters that we have.
|
||||
if (hasOptionalParameters) {
|
||||
if (isDataClass) {
|
||||
result.addCode("«%1N = %1N.copy(", resultName)
|
||||
} else {
|
||||
result.addCode("«%1N = %2T(", resultName, originalTypeName)
|
||||
}
|
||||
separator = "\n"
|
||||
for (property in propertyList) {
|
||||
if (!property.hasConstructorParameter) {
|
||||
continue // No constructor parameter for this property.
|
||||
}
|
||||
if (isDataClass && !property.hasDefault) {
|
||||
continue // Property already assigned.
|
||||
}
|
||||
|
||||
result.addCode(separator)
|
||||
when {
|
||||
property.differentiateAbsentFromNull -> {
|
||||
result.addCode("%2N = if (%3N) %4N else %1N.%2N",
|
||||
resultName, property.name, property.localIsPresentName, property.localName)
|
||||
}
|
||||
property.isRequired -> {
|
||||
result.addCode("%1N = %2N", property.name, property.localName)
|
||||
}
|
||||
else -> {
|
||||
result.addCode("%2N = %3N ?: %1N.%2N", resultName, property.name, property.localName)
|
||||
}
|
||||
}
|
||||
separator = ",\n"
|
||||
}
|
||||
result.addCode("»)\n")
|
||||
}
|
||||
|
||||
// Assign properties not present in the constructor.
|
||||
for (property in propertyList) {
|
||||
if (property.hasConstructorParameter) {
|
||||
continue // Property already handled.
|
||||
}
|
||||
if (property.differentiateAbsentFromNull) {
|
||||
result.addStatement("%1N.%2N = if (%3N) %4N else %1N.%2N",
|
||||
resultName, property.name, property.localIsPresentName, property.localName)
|
||||
} else {
|
||||
result.addStatement("%1N.%2N = %3N ?: %1N.%2N",
|
||||
resultName, property.name, property.localName)
|
||||
}
|
||||
}
|
||||
|
||||
result.addStatement("return %1N", resultName)
|
||||
return result.build()
|
||||
}
|
||||
|
||||
private fun generateToJsonFun(): FunSpec {
|
||||
val result = FunSpec.builder("toJson")
|
||||
.addModifiers(KModifier.OVERRIDE)
|
||||
.addParameter(writerParam)
|
||||
.addParameter(valueParam)
|
||||
|
||||
result.beginControlFlow("if (%N == null)", valueParam)
|
||||
result.addStatement("throw·%T(%S)", NullPointerException::class,
|
||||
"${valueParam.name} was null! Wrap in .nullSafe() to write nullable values.")
|
||||
result.endControlFlow()
|
||||
|
||||
result.addStatement("%N.beginObject()", writerParam)
|
||||
propertyList.forEach { property ->
|
||||
result.addStatement("%N.name(%S)", writerParam, property.jsonName)
|
||||
result.addStatement("%N.toJson(%N, %N.%L)",
|
||||
nameAllocator[property.delegateKey], writerParam, valueParam, property.name)
|
||||
}
|
||||
result.addStatement("%N.endObject()", writerParam)
|
||||
|
||||
return result.build()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.kotlin.codegen
|
||||
|
||||
import com.squareup.kotlinpoet.TypeName
|
||||
import com.squareup.kotlinpoet.TypeVariableName
|
||||
import com.squareup.kotlinpoet.asTypeName
|
||||
import javax.lang.model.element.TypeElement
|
||||
import javax.lang.model.type.DeclaredType
|
||||
import javax.lang.model.util.Types
|
||||
|
||||
/**
|
||||
* A concrete type like `List<String>` with enough information to know how to resolve its type
|
||||
* variables.
|
||||
*/
|
||||
internal class AppliedType private constructor(
|
||||
val element: TypeElement,
|
||||
val resolver: TypeResolver,
|
||||
private val mirror: DeclaredType
|
||||
) {
|
||||
/** Returns all supertypes of this, recursively. Includes both interface and class supertypes. */
|
||||
fun supertypes(
|
||||
types: Types,
|
||||
result: MutableSet<AppliedType> = mutableSetOf()
|
||||
): Set<AppliedType> {
|
||||
result.add(this)
|
||||
for (supertype in types.directSupertypes(mirror)) {
|
||||
val supertypeDeclaredType = supertype as DeclaredType
|
||||
val supertypeElement = supertypeDeclaredType.asElement() as TypeElement
|
||||
val appliedSupertype = AppliedType(supertypeElement,
|
||||
resolver(supertypeElement, supertypeDeclaredType), supertypeDeclaredType)
|
||||
appliedSupertype.supertypes(types, result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/** Returns a resolver that uses `element` and `mirror` to resolve type parameters. */
|
||||
private fun resolver(element: TypeElement, mirror: DeclaredType): TypeResolver {
|
||||
return object : TypeResolver() {
|
||||
override fun resolveTypeVariable(typeVariable: TypeVariableName): TypeName {
|
||||
val index = element.typeParameters.indexOfFirst {
|
||||
it.simpleName.toString() == typeVariable.name
|
||||
}
|
||||
check(index != -1) { "Unexpected type variable $typeVariable in $mirror" }
|
||||
val argument = mirror.typeArguments[index]
|
||||
return argument.asTypeName()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString() = mirror.toString()
|
||||
|
||||
companion object {
|
||||
fun get(typeElement: TypeElement): AppliedType {
|
||||
return AppliedType(typeElement, TypeResolver(), typeElement.asType() as DeclaredType)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.kotlin.codegen
|
||||
|
||||
import com.squareup.kotlinpoet.AnnotationSpec
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.CodeBlock
|
||||
import com.squareup.kotlinpoet.KModifier
|
||||
import com.squareup.kotlinpoet.MemberName
|
||||
import com.squareup.kotlinpoet.NameAllocator
|
||||
import com.squareup.kotlinpoet.ParameterSpec
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
|
||||
import com.squareup.kotlinpoet.PropertySpec
|
||||
import com.squareup.kotlinpoet.TypeName
|
||||
import com.squareup.kotlinpoet.TypeVariableName
|
||||
import com.squareup.kotlinpoet.WildcardTypeName
|
||||
import com.squareup.kotlinpoet.asClassName
|
||||
import com.squareup.kotlinpoet.asTypeName
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Types
|
||||
|
||||
/** A JsonAdapter that can be used to encode and decode a particular field. */
|
||||
internal data class DelegateKey(
|
||||
private val type: TypeName,
|
||||
private val jsonQualifiers: List<AnnotationSpec>
|
||||
) {
|
||||
val nullable get() = type.isNullable
|
||||
|
||||
/** Returns an adapter to use when encoding and decoding this property. */
|
||||
fun generateProperty(
|
||||
nameAllocator: NameAllocator,
|
||||
typeRenderer: TypeRenderer,
|
||||
moshiParameter: ParameterSpec,
|
||||
propertyName: String
|
||||
): PropertySpec {
|
||||
val qualifierNames = jsonQualifiers.joinToString("") {
|
||||
"At${it.className.simpleName}"
|
||||
}
|
||||
val adapterName = nameAllocator.newName(
|
||||
"${type.toVariableName().decapitalize()}${qualifierNames}Adapter", this)
|
||||
|
||||
val adapterTypeName = JsonAdapter::class.asClassName().parameterizedBy(type)
|
||||
val standardArgs = arrayOf(moshiParameter,
|
||||
CodeBlock.of("<%T>", type),
|
||||
typeRenderer.render(type))
|
||||
val (initializerString, args) = when {
|
||||
jsonQualifiers.isEmpty() -> ", %M()" to arrayOf(MemberName("kotlin.collections", "emptySet"))
|
||||
else -> {
|
||||
", %T.getFieldJsonQualifierAnnotations(javaClass, " +
|
||||
"%S)" to arrayOf(Types::class.asTypeName(), adapterName)
|
||||
}
|
||||
}
|
||||
val finalArgs = arrayOf(*standardArgs, *args, propertyName)
|
||||
|
||||
return PropertySpec.builder(adapterName, adapterTypeName, KModifier.PRIVATE)
|
||||
.addAnnotations(jsonQualifiers)
|
||||
.initializer("%N.adapter%L(%L$initializerString, %S)", *finalArgs)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a suggested variable name derived from a list of type names. This just concatenates,
|
||||
* yielding types like MapOfStringLong.
|
||||
*/
|
||||
private fun List<TypeName>.toVariableNames() = joinToString("") { it.toVariableName() }
|
||||
|
||||
/** Returns a suggested variable name derived from a type name, like nullableListOfString. */
|
||||
private fun TypeName.toVariableName(): String {
|
||||
val base = when (this) {
|
||||
is ClassName -> simpleName
|
||||
is ParameterizedTypeName -> rawType.simpleName + "Of" + typeArguments.toVariableNames()
|
||||
is WildcardTypeName -> (inTypes + outTypes).toVariableNames()
|
||||
is TypeVariableName -> name + bounds.toVariableNames()
|
||||
else -> throw IllegalArgumentException("Unrecognized type! $this")
|
||||
}
|
||||
|
||||
return if (isNullable) {
|
||||
"Nullable$base"
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.kotlin.codegen
|
||||
|
||||
import com.google.auto.service.AutoService
|
||||
import com.squareup.moshi.JsonClass
|
||||
import me.eugeniomarletti.kotlin.metadata.KotlinMetadataUtils
|
||||
import me.eugeniomarletti.kotlin.metadata.declaresDefaultValue
|
||||
import me.eugeniomarletti.kotlin.processing.KotlinAbstractProcessor
|
||||
import net.ltgt.gradle.incap.IncrementalAnnotationProcessor
|
||||
import net.ltgt.gradle.incap.IncrementalAnnotationProcessorType.ISOLATING
|
||||
import javax.annotation.processing.ProcessingEnvironment
|
||||
import javax.annotation.processing.Processor
|
||||
import javax.annotation.processing.RoundEnvironment
|
||||
import javax.lang.model.SourceVersion
|
||||
import javax.lang.model.element.Element
|
||||
import javax.lang.model.element.TypeElement
|
||||
import javax.tools.Diagnostic.Kind.ERROR
|
||||
|
||||
/**
|
||||
* An annotation processor that reads Kotlin data classes and generates Moshi JsonAdapters for them.
|
||||
* This generates Kotlin code, and understands basic Kotlin language features like default values
|
||||
* and companion objects.
|
||||
*
|
||||
* The generated class will match the visibility of the given data class (i.e. if it's internal, the
|
||||
* adapter will also be internal).
|
||||
*
|
||||
* If you define a companion object, a jsonAdapter() extension function will be generated onto it.
|
||||
* If you don't want this though, you can use the runtime [JsonClass] factory implementation.
|
||||
*/
|
||||
@AutoService(Processor::class)
|
||||
@IncrementalAnnotationProcessor(ISOLATING)
|
||||
class JsonClassCodegenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* This annotation processing argument can be specified to have a `@Generated` annotation
|
||||
* included in the generated code. It is not encouraged unless you need it for static analysis
|
||||
* reasons and not enabled by default.
|
||||
*
|
||||
* Note that this can only be one of the following values:
|
||||
* * `"javax.annotation.processing.Generated"` (JRE 9+)
|
||||
* * `"javax.annotation.Generated"` (JRE <9)
|
||||
*/
|
||||
const val OPTION_GENERATED = "moshi.generated"
|
||||
private val POSSIBLE_GENERATED_NAMES = setOf(
|
||||
"javax.annotation.processing.Generated",
|
||||
"javax.annotation.Generated"
|
||||
)
|
||||
}
|
||||
|
||||
private val annotation = JsonClass::class.java
|
||||
private var generatedType: TypeElement? = null
|
||||
|
||||
override fun getSupportedAnnotationTypes() = setOf(annotation.canonicalName)
|
||||
|
||||
override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latest()
|
||||
|
||||
override fun getSupportedOptions() = setOf(OPTION_GENERATED)
|
||||
|
||||
override fun init(processingEnv: ProcessingEnvironment) {
|
||||
super.init(processingEnv)
|
||||
generatedType = processingEnv.options[OPTION_GENERATED]?.let {
|
||||
if (it !in POSSIBLE_GENERATED_NAMES) {
|
||||
throw IllegalArgumentException("Invalid option value for $OPTION_GENERATED. Found $it, " +
|
||||
"allowable values are $POSSIBLE_GENERATED_NAMES.")
|
||||
}
|
||||
processingEnv.elementUtils.getTypeElement(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
|
||||
for (type in roundEnv.getElementsAnnotatedWith(annotation)) {
|
||||
val jsonClass = type.getAnnotation(annotation)
|
||||
if (jsonClass.generateAdapter && jsonClass.generator.isEmpty()) {
|
||||
val generator = adapterGenerator(type) ?: continue
|
||||
generator.generateFile(generatedType)
|
||||
.writeTo(filer)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun adapterGenerator(element: Element): AdapterGenerator? {
|
||||
val type = TargetType.get(messager, elementUtils, typeUtils, element) ?: return null
|
||||
|
||||
val properties = mutableMapOf<String, PropertyGenerator>()
|
||||
for (property in type.properties.values) {
|
||||
val generator = property.generator(messager)
|
||||
if (generator != null) {
|
||||
properties[property.name] = generator
|
||||
}
|
||||
}
|
||||
|
||||
for ((name, parameter) in type.constructor.parameters) {
|
||||
if (type.properties[parameter.name] == null && !parameter.proto.declaresDefaultValue) {
|
||||
messager.printMessage(
|
||||
ERROR, "No property for required constructor parameter $name", parameter.element)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Sort properties so that those with constructor parameters come first.
|
||||
val sortedProperties = properties.values.sortedBy {
|
||||
if (it.hasConstructorParameter) {
|
||||
it.target.parameterIndex
|
||||
} else {
|
||||
Integer.MAX_VALUE
|
||||
}
|
||||
}
|
||||
|
||||
return AdapterGenerator(type, sortedProperties)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.kotlin.codegen
|
||||
|
||||
import com.squareup.kotlinpoet.BOOLEAN
|
||||
import com.squareup.kotlinpoet.NameAllocator
|
||||
import com.squareup.kotlinpoet.PropertySpec
|
||||
|
||||
/** Generates functions to encode and decode a property as JSON. */
|
||||
internal class PropertyGenerator(
|
||||
val target: TargetProperty,
|
||||
val delegateKey: DelegateKey
|
||||
) {
|
||||
val name = target.name
|
||||
val jsonName = target.jsonName()
|
||||
val hasDefault = target.hasDefault
|
||||
|
||||
lateinit var localName: String
|
||||
lateinit var localIsPresentName: String
|
||||
|
||||
val isRequired get() = !delegateKey.nullable && !hasDefault
|
||||
|
||||
val hasConstructorParameter get() = target.parameterIndex != -1
|
||||
|
||||
/** We prefer to use 'null' to mean absent, but for some properties those are distinct. */
|
||||
val differentiateAbsentFromNull get() = delegateKey.nullable && hasDefault
|
||||
|
||||
fun allocateNames(nameAllocator: NameAllocator) {
|
||||
localName = nameAllocator.newName(name)
|
||||
localIsPresentName = nameAllocator.newName("${name}Set")
|
||||
}
|
||||
|
||||
fun generateLocalProperty(): PropertySpec {
|
||||
return PropertySpec.builder(localName, target.type.copy(nullable = true))
|
||||
.mutable(true)
|
||||
.initializer("null")
|
||||
.build()
|
||||
}
|
||||
|
||||
fun generateLocalIsPresentProperty(): PropertySpec {
|
||||
return PropertySpec.builder(localIsPresentName, BOOLEAN)
|
||||
.mutable(true)
|
||||
.initializer("false")
|
||||
.build()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.kotlin.codegen
|
||||
|
||||
import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
|
||||
import me.eugeniomarletti.kotlin.metadata.isPrimary
|
||||
import me.eugeniomarletti.kotlin.metadata.jvm.getJvmConstructorSignature
|
||||
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Constructor
|
||||
import javax.lang.model.element.ElementKind
|
||||
import javax.lang.model.element.ExecutableElement
|
||||
import javax.lang.model.util.Elements
|
||||
|
||||
/** A constructor in user code that should be called by generated code. */
|
||||
internal data class TargetConstructor(
|
||||
val element: ExecutableElement,
|
||||
val proto: Constructor,
|
||||
val parameters: Map<String, TargetParameter>
|
||||
) {
|
||||
companion object {
|
||||
fun primary(metadata: KotlinClassMetadata, elements: Elements): TargetConstructor {
|
||||
val (nameResolver, classProto) = metadata.data
|
||||
|
||||
// todo allow custom constructor
|
||||
val proto = classProto.constructorList
|
||||
.single { it.isPrimary }
|
||||
val constructorJvmSignature = proto.getJvmConstructorSignature(
|
||||
nameResolver, classProto.typeTable)
|
||||
val element = classProto.fqName
|
||||
.let(nameResolver::getString)
|
||||
.replace('/', '.')
|
||||
.let(elements::getTypeElement)
|
||||
.enclosedElements
|
||||
.mapNotNull {
|
||||
it.takeIf { it.kind == ElementKind.CONSTRUCTOR }?.let { it as ExecutableElement }
|
||||
}
|
||||
.first()
|
||||
// TODO Temporary until JVM method signature matching is better
|
||||
// .single { it.jvmMethodSignature == constructorJvmSignature }
|
||||
|
||||
val parameters = mutableMapOf<String, TargetParameter>()
|
||||
for (parameter in proto.valueParameterList) {
|
||||
val name = nameResolver.getString(parameter.name)
|
||||
val index = proto.valueParameterList.indexOf(parameter)
|
||||
parameters[name] = TargetParameter(name, parameter, index, element.parameters[index])
|
||||
}
|
||||
|
||||
return TargetConstructor(element, proto, parameters)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.kotlin.codegen
|
||||
|
||||
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.ValueParameter
|
||||
import javax.lang.model.element.VariableElement
|
||||
|
||||
/** A parameter in user code that should be populated by generated code. */
|
||||
internal data class TargetParameter(
|
||||
val name: String,
|
||||
val proto: ValueParameter,
|
||||
val index: Int,
|
||||
val element: VariableElement
|
||||
)
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.kotlin.codegen
|
||||
|
||||
import com.google.auto.common.AnnotationMirrors
|
||||
import com.google.auto.common.MoreTypes
|
||||
import com.squareup.kotlinpoet.AnnotationSpec
|
||||
import com.squareup.kotlinpoet.TypeName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonQualifier
|
||||
import me.eugeniomarletti.kotlin.metadata.declaresDefaultValue
|
||||
import me.eugeniomarletti.kotlin.metadata.hasSetter
|
||||
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Property
|
||||
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.INTERNAL
|
||||
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.PROTECTED
|
||||
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.PUBLIC
|
||||
import me.eugeniomarletti.kotlin.metadata.visibility
|
||||
import java.lang.annotation.ElementType
|
||||
import java.lang.annotation.Retention
|
||||
import java.lang.annotation.RetentionPolicy
|
||||
import java.lang.annotation.Target
|
||||
import javax.annotation.processing.Messager
|
||||
import javax.lang.model.element.AnnotationMirror
|
||||
import javax.lang.model.element.Element
|
||||
import javax.lang.model.element.ExecutableElement
|
||||
import javax.lang.model.element.Modifier
|
||||
import javax.lang.model.element.Name
|
||||
import javax.lang.model.element.VariableElement
|
||||
import javax.tools.Diagnostic
|
||||
|
||||
/** A property in user code that maps to JSON. */
|
||||
internal data class TargetProperty(
|
||||
val name: String,
|
||||
val type: TypeName,
|
||||
private val proto: Property,
|
||||
private val parameter: TargetParameter?,
|
||||
private val annotationHolder: ExecutableElement?,
|
||||
private val field: VariableElement?,
|
||||
private val setter: ExecutableElement?,
|
||||
private val getter: ExecutableElement?
|
||||
) {
|
||||
val parameterIndex get() = parameter?.index ?: -1
|
||||
|
||||
val hasDefault get() = parameter?.proto?.declaresDefaultValue ?: true
|
||||
|
||||
private val isTransient get() = field != null && Modifier.TRANSIENT in field.modifiers
|
||||
|
||||
private val element get() = field ?: setter ?: getter!!
|
||||
|
||||
private val isSettable get() = proto.hasSetter || parameter != null
|
||||
|
||||
private val isVisible: Boolean
|
||||
get() {
|
||||
return proto.visibility == INTERNAL
|
||||
|| proto.visibility == PROTECTED
|
||||
|| proto.visibility == PUBLIC
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a generator for this property, or null if either there is an error and this property
|
||||
* cannot be used with code gen, or if no codegen is necessary for this property.
|
||||
*/
|
||||
fun generator(messager: Messager): PropertyGenerator? {
|
||||
if (isTransient) {
|
||||
if (!hasDefault) {
|
||||
messager.printMessage(
|
||||
Diagnostic.Kind.ERROR, "No default value for transient property ${this}", element)
|
||||
return null
|
||||
}
|
||||
return null // This property is transient and has a default value. Ignore it.
|
||||
}
|
||||
|
||||
if (!isVisible) {
|
||||
messager.printMessage(Diagnostic.Kind.ERROR, "property ${this} is not visible", element)
|
||||
return null
|
||||
}
|
||||
|
||||
if (!isSettable) {
|
||||
return null // This property is not settable. Ignore it.
|
||||
}
|
||||
|
||||
val jsonQualifierMirrors = jsonQualifiers()
|
||||
for (jsonQualifier in jsonQualifierMirrors) {
|
||||
// Check Java types since that covers both Java and Kotlin annotations.
|
||||
val annotationElement = MoreTypes.asTypeElement(jsonQualifier.annotationType)
|
||||
annotationElement.getAnnotation(Retention::class.java)?.let {
|
||||
if (it.value != RetentionPolicy.RUNTIME) {
|
||||
messager.printMessage(Diagnostic.Kind.ERROR,
|
||||
"JsonQualifier @${jsonQualifier.simpleName} must have RUNTIME retention")
|
||||
}
|
||||
}
|
||||
annotationElement.getAnnotation(Target::class.java)?.let {
|
||||
if (ElementType.FIELD !in it.value) {
|
||||
messager.printMessage(Diagnostic.Kind.ERROR,
|
||||
"JsonQualifier @${jsonQualifier.simpleName} must support FIELD target")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val jsonQualifierSpecs = jsonQualifierMirrors.map {
|
||||
AnnotationSpec.get(it).toBuilder()
|
||||
.useSiteTarget(AnnotationSpec.UseSiteTarget.FIELD)
|
||||
.build()
|
||||
}
|
||||
|
||||
return PropertyGenerator(this, DelegateKey(type, jsonQualifierSpecs))
|
||||
}
|
||||
|
||||
/** Returns the JsonQualifiers on the field and parameter of this property. */
|
||||
private fun jsonQualifiers(): Set<AnnotationMirror> {
|
||||
val elementQualifiers = element.qualifiers
|
||||
val annotationHolderQualifiers = annotationHolder.qualifiers
|
||||
val parameterQualifiers = parameter?.element.qualifiers
|
||||
|
||||
// TODO(jwilson): union the qualifiers somehow?
|
||||
return when {
|
||||
elementQualifiers.isNotEmpty() -> elementQualifiers
|
||||
annotationHolderQualifiers.isNotEmpty() -> annotationHolderQualifiers
|
||||
parameterQualifiers.isNotEmpty() -> parameterQualifiers
|
||||
else -> setOf()
|
||||
}
|
||||
}
|
||||
|
||||
private val Element?.qualifiers: Set<AnnotationMirror>
|
||||
get() {
|
||||
if (this == null) return setOf()
|
||||
return AnnotationMirrors.getAnnotatedAnnotations(this, JsonQualifier::class.java)
|
||||
}
|
||||
|
||||
/** Returns the @Json name of this property, or this property's name if none is provided. */
|
||||
fun jsonName(): String {
|
||||
val fieldJsonName = element.jsonName
|
||||
val annotationHolderJsonName = annotationHolder.jsonName
|
||||
val parameterJsonName = parameter?.element.jsonName
|
||||
|
||||
return when {
|
||||
fieldJsonName != null -> fieldJsonName
|
||||
annotationHolderJsonName != null -> annotationHolderJsonName
|
||||
parameterJsonName != null -> parameterJsonName
|
||||
else -> name
|
||||
}
|
||||
}
|
||||
|
||||
private val Element?.jsonName: String?
|
||||
get() {
|
||||
if (this == null) return null
|
||||
return getAnnotation(Json::class.java)?.name
|
||||
}
|
||||
|
||||
private val AnnotationMirror.simpleName: Name
|
||||
get() = MoreTypes.asTypeElement(annotationType).simpleName!!
|
||||
|
||||
override fun toString() = name
|
||||
}
|
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.kotlin.codegen
|
||||
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.KModifier
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName
|
||||
import com.squareup.kotlinpoet.TypeVariableName
|
||||
import com.squareup.kotlinpoet.asClassName
|
||||
import com.squareup.kotlinpoet.asTypeName
|
||||
import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
|
||||
import me.eugeniomarletti.kotlin.metadata.KotlinMetadata
|
||||
import me.eugeniomarletti.kotlin.metadata.classKind
|
||||
import me.eugeniomarletti.kotlin.metadata.getPropertyOrNull
|
||||
import me.eugeniomarletti.kotlin.metadata.isInnerClass
|
||||
import me.eugeniomarletti.kotlin.metadata.kotlinMetadata
|
||||
import me.eugeniomarletti.kotlin.metadata.modality
|
||||
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Class
|
||||
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Modality.ABSTRACT
|
||||
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.TypeParameter
|
||||
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.INTERNAL
|
||||
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.LOCAL
|
||||
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.PUBLIC
|
||||
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.deserialization.NameResolver
|
||||
import me.eugeniomarletti.kotlin.metadata.shadow.util.capitalizeDecapitalize.decapitalizeAsciiOnly
|
||||
import me.eugeniomarletti.kotlin.metadata.visibility
|
||||
import javax.annotation.processing.Messager
|
||||
import javax.lang.model.element.Element
|
||||
import javax.lang.model.element.ElementKind
|
||||
import javax.lang.model.element.ExecutableElement
|
||||
import javax.lang.model.element.TypeElement
|
||||
import javax.lang.model.element.VariableElement
|
||||
import javax.lang.model.util.Elements
|
||||
import javax.lang.model.util.Types
|
||||
import javax.tools.Diagnostic.Kind.ERROR
|
||||
|
||||
/** A user type that should be decoded and encoded by generated code. */
|
||||
internal data class TargetType(
|
||||
val proto: Class,
|
||||
val element: TypeElement,
|
||||
val constructor: TargetConstructor,
|
||||
val properties: Map<String, TargetProperty>,
|
||||
val typeVariables: List<TypeVariableName>
|
||||
) {
|
||||
val name = element.className
|
||||
|
||||
companion object {
|
||||
private val OBJECT_CLASS = ClassName("java.lang", "Object")
|
||||
|
||||
/** Returns a target type for `element`, or null if it cannot be used with code gen. */
|
||||
fun get(messager: Messager, elements: Elements, types: Types, element: Element): TargetType? {
|
||||
val typeMetadata: KotlinMetadata? = element.kotlinMetadata
|
||||
if (element !is TypeElement || typeMetadata !is KotlinClassMetadata) {
|
||||
messager.printMessage(
|
||||
ERROR, "@JsonClass can't be applied to $element: must be a Kotlin class", element)
|
||||
return null
|
||||
}
|
||||
|
||||
val proto = typeMetadata.data.classProto
|
||||
when {
|
||||
proto.classKind == Class.Kind.ENUM_CLASS -> {
|
||||
messager.printMessage(
|
||||
ERROR, "@JsonClass with 'generateAdapter = \"true\"' can't be applied to $element: code gen for enums is not supported or necessary", element)
|
||||
return null
|
||||
}
|
||||
proto.classKind != Class.Kind.CLASS -> {
|
||||
messager.printMessage(
|
||||
ERROR, "@JsonClass can't be applied to $element: must be a Kotlin class", element)
|
||||
return null
|
||||
}
|
||||
proto.isInnerClass -> {
|
||||
messager.printMessage(
|
||||
ERROR, "@JsonClass can't be applied to $element: must not be an inner class", element)
|
||||
return null
|
||||
}
|
||||
proto.modality == ABSTRACT -> {
|
||||
messager.printMessage(
|
||||
ERROR, "@JsonClass can't be applied to $element: must not be abstract", element)
|
||||
return null
|
||||
}
|
||||
proto.visibility == LOCAL -> {
|
||||
messager.printMessage(
|
||||
ERROR, "@JsonClass can't be applied to $element: must not be local", element)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
val typeVariables = genericTypeNames(proto, typeMetadata.data.nameResolver)
|
||||
val appliedType = AppliedType.get(element)
|
||||
|
||||
val constructor = TargetConstructor.primary(typeMetadata, elements)
|
||||
if (constructor.proto.visibility != INTERNAL && constructor.proto.visibility != PUBLIC) {
|
||||
messager.printMessage(ERROR, "@JsonClass can't be applied to $element: " +
|
||||
"primary constructor is not internal or public", element)
|
||||
return null
|
||||
}
|
||||
|
||||
val properties = mutableMapOf<String, TargetProperty>()
|
||||
for (supertype in appliedType.supertypes(types)) {
|
||||
if (supertype.element.asClassName() == OBJECT_CLASS) {
|
||||
continue // Don't load properties for java.lang.Object.
|
||||
}
|
||||
if (supertype.element.kind != ElementKind.CLASS) {
|
||||
continue // Don't load properties for interface types.
|
||||
}
|
||||
if (supertype.element.kotlinMetadata == null) {
|
||||
messager.printMessage(ERROR,
|
||||
"@JsonClass can't be applied to $element: supertype $supertype is not a Kotlin type",
|
||||
element)
|
||||
return null
|
||||
}
|
||||
val supertypeProperties = declaredProperties(
|
||||
supertype.element, supertype.resolver, constructor)
|
||||
for ((name, property) in supertypeProperties) {
|
||||
properties.putIfAbsent(name, property)
|
||||
}
|
||||
}
|
||||
return TargetType(proto, element, constructor, properties, typeVariables)
|
||||
}
|
||||
|
||||
/** Returns the properties declared by `typeElement`. */
|
||||
private fun declaredProperties(
|
||||
typeElement: TypeElement,
|
||||
typeResolver: TypeResolver,
|
||||
constructor: TargetConstructor
|
||||
): Map<String, TargetProperty> {
|
||||
val typeMetadata: KotlinClassMetadata = typeElement.kotlinMetadata as KotlinClassMetadata
|
||||
val nameResolver = typeMetadata.data.nameResolver
|
||||
val classProto = typeMetadata.data.classProto
|
||||
|
||||
val annotationHolders = mutableMapOf<String, ExecutableElement>()
|
||||
val fields = mutableMapOf<String, VariableElement>()
|
||||
val setters = mutableMapOf<String, ExecutableElement>()
|
||||
val getters = mutableMapOf<String, ExecutableElement>()
|
||||
for (element in typeElement.enclosedElements) {
|
||||
if (element is VariableElement) {
|
||||
fields[element.name] = element
|
||||
} else if (element is ExecutableElement) {
|
||||
when {
|
||||
element.name.startsWith("get") -> {
|
||||
val name = element.name.substring("get".length).decapitalizeAsciiOnly()
|
||||
getters[name] = element
|
||||
}
|
||||
element.name.startsWith("is") -> {
|
||||
val name = element.name.substring("is".length).decapitalizeAsciiOnly()
|
||||
getters[name] = element
|
||||
}
|
||||
element.name.startsWith("set") -> {
|
||||
val name = element.name.substring("set".length).decapitalizeAsciiOnly()
|
||||
setters[name] = element
|
||||
}
|
||||
}
|
||||
|
||||
val propertyProto = typeMetadata.data.getPropertyOrNull(element)
|
||||
if (propertyProto != null) {
|
||||
val name = nameResolver.getString(propertyProto.name)
|
||||
annotationHolders[name] = element
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val result = mutableMapOf<String, TargetProperty>()
|
||||
for (property in classProto.propertyList) {
|
||||
val name = nameResolver.getString(property.name)
|
||||
val type = typeResolver.resolve(property.returnType.asTypeName(
|
||||
nameResolver, classProto::getTypeParameter, false))
|
||||
result[name] = TargetProperty(name, type, property, constructor.parameters[name],
|
||||
annotationHolders[name], fields[name], setters[name], getters[name])
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private val Element.className: ClassName
|
||||
get() {
|
||||
val typeName = asType().asTypeName()
|
||||
return when (typeName) {
|
||||
is ClassName -> typeName
|
||||
is ParameterizedTypeName -> typeName.rawType
|
||||
else -> throw IllegalStateException("unexpected TypeName: ${typeName::class}")
|
||||
}
|
||||
}
|
||||
|
||||
private val Element.name get() = simpleName.toString()
|
||||
|
||||
private fun genericTypeNames(proto: Class, nameResolver: NameResolver): List<TypeVariableName> {
|
||||
return proto.typeParameterList.map {
|
||||
val possibleBounds = it.upperBoundList
|
||||
.map { it.asTypeName(nameResolver, proto::getTypeParameter, false) }
|
||||
val typeVar = if (possibleBounds.isEmpty()) {
|
||||
TypeVariableName(
|
||||
name = nameResolver.getString(it.name),
|
||||
variance = it.varianceModifier)
|
||||
} else {
|
||||
TypeVariableName(
|
||||
name = nameResolver.getString(it.name),
|
||||
bounds = *possibleBounds.toTypedArray(),
|
||||
variance = it.varianceModifier)
|
||||
}
|
||||
return@map typeVar.copy(reified = it.reified)
|
||||
}
|
||||
}
|
||||
|
||||
private val TypeParameter.varianceModifier: KModifier?
|
||||
get() {
|
||||
return variance.asKModifier().let {
|
||||
// We don't redeclare out variance here
|
||||
if (it == KModifier.OUT) {
|
||||
null
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.kotlin.codegen
|
||||
|
||||
import com.squareup.kotlinpoet.ARRAY
|
||||
import com.squareup.kotlinpoet.BOOLEAN
|
||||
import com.squareup.kotlinpoet.BYTE
|
||||
import com.squareup.kotlinpoet.CHAR
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.CodeBlock
|
||||
import com.squareup.kotlinpoet.DOUBLE
|
||||
import com.squareup.kotlinpoet.FLOAT
|
||||
import com.squareup.kotlinpoet.INT
|
||||
import com.squareup.kotlinpoet.LONG
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName
|
||||
import com.squareup.kotlinpoet.SHORT
|
||||
import com.squareup.kotlinpoet.TypeName
|
||||
import com.squareup.kotlinpoet.TypeVariableName
|
||||
import com.squareup.kotlinpoet.WildcardTypeName
|
||||
import com.squareup.moshi.Types
|
||||
|
||||
/**
|
||||
* Renders literals like `Types.newParameterizedType(List::class.java, String::class.java)`.
|
||||
* Rendering is pluggable so that type variables can either be resolved or emitted as other code
|
||||
* blocks.
|
||||
*/
|
||||
abstract class TypeRenderer {
|
||||
abstract fun renderTypeVariable(typeVariable: TypeVariableName): CodeBlock
|
||||
|
||||
fun render(typeName: TypeName): CodeBlock {
|
||||
if (typeName.isNullable) {
|
||||
return renderObjectType(typeName.copy(nullable = false))
|
||||
}
|
||||
|
||||
return when (typeName) {
|
||||
is ClassName -> CodeBlock.of("%T::class.java", typeName)
|
||||
|
||||
is ParameterizedTypeName -> {
|
||||
// If it's an Array type, we shortcut this to return Types.arrayOf()
|
||||
if (typeName.rawType == ARRAY) {
|
||||
CodeBlock.of("%T.arrayOf(%L)",
|
||||
Types::class,
|
||||
renderObjectType(typeName.typeArguments[0]))
|
||||
} else {
|
||||
val builder = CodeBlock.builder().apply {
|
||||
add("%T.", Types::class)
|
||||
val enclosingClassName = typeName.rawType.enclosingClassName()
|
||||
if (enclosingClassName != null) {
|
||||
add("newParameterizedTypeWithOwner(%L, ", render(enclosingClassName))
|
||||
} else {
|
||||
add("newParameterizedType(")
|
||||
}
|
||||
add("%T::class.java", typeName.rawType)
|
||||
for (typeArgument in typeName.typeArguments) {
|
||||
add(", %L", renderObjectType(typeArgument))
|
||||
}
|
||||
add(")")
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
}
|
||||
|
||||
is WildcardTypeName -> {
|
||||
val target: TypeName
|
||||
val method: String
|
||||
when {
|
||||
typeName.inTypes.size == 1 -> {
|
||||
target = typeName.inTypes[0]
|
||||
method = "supertypeOf"
|
||||
}
|
||||
typeName.outTypes.size == 1 -> {
|
||||
target = typeName.outTypes[0]
|
||||
method = "subtypeOf"
|
||||
}
|
||||
else -> throw IllegalArgumentException(
|
||||
"Unrepresentable wildcard type. Cannot have more than one bound: $typeName")
|
||||
}
|
||||
CodeBlock.of("%T.%L(%T::class.java)", Types::class, method, target.copy(nullable = false))
|
||||
}
|
||||
|
||||
is TypeVariableName -> renderTypeVariable(typeName)
|
||||
|
||||
else -> throw IllegalArgumentException("Unrepresentable type: $typeName")
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderObjectType(typeName: TypeName): CodeBlock {
|
||||
return if (typeName.isPrimitive()) {
|
||||
CodeBlock.of("%T::class.javaObjectType", typeName)
|
||||
} else {
|
||||
render(typeName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TypeName.isPrimitive(): Boolean {
|
||||
return when (this) {
|
||||
BOOLEAN, BYTE, SHORT, INT, LONG, CHAR, FLOAT, DOUBLE -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.kotlin.codegen
|
||||
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
|
||||
import com.squareup.kotlinpoet.TypeName
|
||||
import com.squareup.kotlinpoet.TypeVariableName
|
||||
import com.squareup.kotlinpoet.WildcardTypeName
|
||||
|
||||
/**
|
||||
* Resolves type parameters against a type declaration. Use this to fill in type variables with
|
||||
* their actual type parameters.
|
||||
*/
|
||||
open class TypeResolver {
|
||||
open fun resolveTypeVariable(typeVariable: TypeVariableName): TypeName = typeVariable
|
||||
|
||||
fun resolve(typeName: TypeName): TypeName {
|
||||
return when (typeName) {
|
||||
is ClassName -> typeName
|
||||
|
||||
is ParameterizedTypeName -> {
|
||||
typeName.rawType.parameterizedBy(*(typeName.typeArguments.map { resolve(it) }.toTypedArray()))
|
||||
.copy(nullable = typeName.isNullable)
|
||||
}
|
||||
|
||||
is WildcardTypeName -> {
|
||||
when {
|
||||
typeName.inTypes.size == 1 -> {
|
||||
WildcardTypeName.consumerOf(resolve(typeName.inTypes[0]))
|
||||
.copy(nullable = typeName.isNullable)
|
||||
}
|
||||
typeName.outTypes.size == 1 -> {
|
||||
WildcardTypeName.producerOf(resolve(typeName.outTypes[0]))
|
||||
.copy(nullable = typeName.isNullable)
|
||||
}
|
||||
else -> {
|
||||
throw IllegalArgumentException(
|
||||
"Unrepresentable wildcard type. Cannot have more than one bound: $typeName")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is TypeVariableName -> resolveTypeVariable(typeName)
|
||||
|
||||
else -> throw IllegalArgumentException("Unrepresentable type: $typeName")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.kotlin.codegen
|
||||
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName
|
||||
import com.squareup.kotlinpoet.TypeName
|
||||
|
||||
internal fun TypeName.rawType(): ClassName {
|
||||
return when (this) {
|
||||
is ClassName -> this
|
||||
is ParameterizedTypeName -> rawType
|
||||
else -> throw IllegalArgumentException("Cannot get raw type from $this")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.kotlin.codegen
|
||||
|
||||
import com.squareup.kotlinpoet.ClassName
|
||||
import com.squareup.kotlinpoet.KModifier
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
|
||||
import com.squareup.kotlinpoet.STAR
|
||||
import com.squareup.kotlinpoet.TypeName
|
||||
import com.squareup.kotlinpoet.TypeVariableName
|
||||
import com.squareup.kotlinpoet.WildcardTypeName
|
||||
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Type
|
||||
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.TypeParameter
|
||||
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.TypeParameter.Variance
|
||||
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.deserialization.NameResolver
|
||||
|
||||
internal fun TypeParameter.asTypeName(
|
||||
nameResolver: NameResolver,
|
||||
getTypeParameter: (index: Int) -> TypeParameter,
|
||||
resolveAliases: Boolean = false
|
||||
): TypeVariableName {
|
||||
val possibleBounds = upperBoundList.map {
|
||||
it.asTypeName(nameResolver, getTypeParameter, resolveAliases)
|
||||
}
|
||||
return if (possibleBounds.isEmpty()) {
|
||||
TypeVariableName(
|
||||
name = nameResolver.getString(name),
|
||||
variance = variance.asKModifier())
|
||||
} else {
|
||||
TypeVariableName(
|
||||
name = nameResolver.getString(name),
|
||||
bounds = *possibleBounds.toTypedArray(),
|
||||
variance = variance.asKModifier())
|
||||
}
|
||||
}
|
||||
|
||||
internal fun TypeParameter.Variance.asKModifier(): KModifier? {
|
||||
return when (this) {
|
||||
Variance.IN -> KModifier.IN
|
||||
Variance.OUT -> KModifier.OUT
|
||||
Variance.INV -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the TypeName of this type as it would be seen in the source code, including nullability
|
||||
* and generic type parameters.
|
||||
*
|
||||
* @param [nameResolver] a [NameResolver] instance from the source proto
|
||||
* @param [getTypeParameter] a function that returns the type parameter for the given index. **Only
|
||||
* called if [ProtoBuf.Type.hasTypeParameter] is true!**
|
||||
*/
|
||||
internal fun Type.asTypeName(
|
||||
nameResolver: NameResolver,
|
||||
getTypeParameter: (index: Int) -> TypeParameter,
|
||||
useAbbreviatedType: Boolean = true
|
||||
): TypeName {
|
||||
|
||||
val argumentList = when {
|
||||
useAbbreviatedType && hasAbbreviatedType() -> abbreviatedType.argumentList
|
||||
else -> argumentList
|
||||
}
|
||||
|
||||
if (hasFlexibleUpperBound()) {
|
||||
return WildcardTypeName.producerOf(
|
||||
flexibleUpperBound.asTypeName(nameResolver, getTypeParameter, useAbbreviatedType))
|
||||
.copy(nullable = nullable)
|
||||
} else if (hasOuterType()) {
|
||||
return WildcardTypeName.consumerOf(
|
||||
outerType.asTypeName(nameResolver, getTypeParameter, useAbbreviatedType))
|
||||
.copy(nullable = nullable)
|
||||
}
|
||||
|
||||
val realType = when {
|
||||
hasTypeParameter() -> return getTypeParameter(typeParameter)
|
||||
.asTypeName(nameResolver, getTypeParameter, useAbbreviatedType)
|
||||
.copy(nullable = nullable)
|
||||
hasTypeParameterName() -> typeParameterName
|
||||
useAbbreviatedType && hasAbbreviatedType() -> abbreviatedType.typeAliasName
|
||||
else -> className
|
||||
}
|
||||
|
||||
var typeName: TypeName =
|
||||
ClassName.bestGuess(nameResolver.getString(realType)
|
||||
.replace("/", "."))
|
||||
|
||||
if (argumentList.isNotEmpty()) {
|
||||
val remappedArgs: Array<TypeName> = argumentList.map { argumentType ->
|
||||
val nullableProjection = if (argumentType.hasProjection()) {
|
||||
argumentType.projection
|
||||
} else null
|
||||
if (argumentType.hasType()) {
|
||||
argumentType.type.asTypeName(nameResolver, getTypeParameter, useAbbreviatedType)
|
||||
.let { argumentTypeName ->
|
||||
nullableProjection?.let { projection ->
|
||||
when (projection) {
|
||||
Type.Argument.Projection.IN -> WildcardTypeName.consumerOf(argumentTypeName)
|
||||
Type.Argument.Projection.OUT -> WildcardTypeName.producerOf(argumentTypeName)
|
||||
Type.Argument.Projection.STAR -> STAR
|
||||
Type.Argument.Projection.INV -> TODO("INV projection is unsupported")
|
||||
}
|
||||
} ?: argumentTypeName
|
||||
}
|
||||
} else {
|
||||
STAR
|
||||
}
|
||||
}.toTypedArray()
|
||||
typeName = (typeName as ClassName).parameterizedBy(*remappedArgs)
|
||||
}
|
||||
|
||||
return typeName.copy(nullable = nullable)
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.kotlin.codegen;
|
||||
|
||||
/** For {@link JsonClassCodegenProcessorTest#extendJavaType}. */
|
||||
public class JavaSuperclass {
|
||||
public int a = 1;
|
||||
}
|
|
@ -0,0 +1,381 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.kotlin.codegen
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.jetbrains.kotlin.cli.common.ExitCode
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import javax.annotation.processing.Processor
|
||||
|
||||
/** Execute kotlinc to confirm that either files are generated or errors are printed. */
|
||||
class JsonClassCodegenProcessorTest {
|
||||
@Rule @JvmField var temporaryFolder: TemporaryFolder = TemporaryFolder()
|
||||
|
||||
@Test fun privateConstructor() {
|
||||
val call = KotlinCompilerCall(temporaryFolder.root)
|
||||
call.inheritClasspath = true
|
||||
call.addService(Processor::class, JsonClassCodegenProcessor::class)
|
||||
call.addKt("source.kt", """
|
||||
|import com.squareup.moshi.JsonClass
|
||||
|
|
||||
|@JsonClass(generateAdapter = true)
|
||||
|class PrivateConstructor private constructor(var a: Int, var b: Int) {
|
||||
| fun a() = a
|
||||
| fun b() = b
|
||||
| companion object {
|
||||
| fun newInstance(a: Int, b: Int) = PrivateConstructor(a, b)
|
||||
| }
|
||||
|}
|
||||
|""".trimMargin())
|
||||
|
||||
val result = call.execute()
|
||||
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
|
||||
assertThat(result.systemErr).contains("constructor is not internal or public")
|
||||
}
|
||||
|
||||
@Test fun privateConstructorParameter() {
|
||||
val call = KotlinCompilerCall(temporaryFolder.root)
|
||||
call.inheritClasspath = true
|
||||
call.addService(Processor::class, JsonClassCodegenProcessor::class)
|
||||
call.addKt("source.kt", """
|
||||
|import com.squareup.moshi.JsonClass
|
||||
|
|
||||
|@JsonClass(generateAdapter = true)
|
||||
|class PrivateConstructorParameter(private var a: Int)
|
||||
|""".trimMargin())
|
||||
|
||||
val result = call.execute()
|
||||
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
|
||||
assertThat(result.systemErr).contains("property a is not visible")
|
||||
}
|
||||
|
||||
@Test fun privateProperties() {
|
||||
val call = KotlinCompilerCall(temporaryFolder.root)
|
||||
call.inheritClasspath = true
|
||||
call.addService(Processor::class, JsonClassCodegenProcessor::class)
|
||||
call.addKt("source.kt", """
|
||||
|import com.squareup.moshi.JsonClass
|
||||
|
|
||||
|@JsonClass(generateAdapter = true)
|
||||
|class PrivateProperties {
|
||||
| private var a: Int = -1
|
||||
| private var b: Int = -1
|
||||
|}
|
||||
|""".trimMargin())
|
||||
|
||||
val result = call.execute()
|
||||
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
|
||||
assertThat(result.systemErr).contains("property a is not visible")
|
||||
}
|
||||
|
||||
@Test fun interfacesNotSupported() {
|
||||
val call = KotlinCompilerCall(temporaryFolder.root)
|
||||
call.inheritClasspath = true
|
||||
call.addService(Processor::class, JsonClassCodegenProcessor::class)
|
||||
call.addKt("source.kt", """
|
||||
|import com.squareup.moshi.JsonClass
|
||||
|
|
||||
|@JsonClass(generateAdapter = true)
|
||||
|interface Interface
|
||||
|""".trimMargin())
|
||||
|
||||
val result = call.execute()
|
||||
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
|
||||
assertThat(result.systemErr).contains(
|
||||
"error: @JsonClass can't be applied to Interface: must be a Kotlin class")
|
||||
}
|
||||
|
||||
@Test fun abstractClassesNotSupported() {
|
||||
val call = KotlinCompilerCall(temporaryFolder.root)
|
||||
call.inheritClasspath = true
|
||||
call.addService(Processor::class, JsonClassCodegenProcessor::class)
|
||||
call.addKt("source.kt", """
|
||||
|import com.squareup.moshi.JsonClass
|
||||
|
|
||||
|@JsonClass(generateAdapter = true)
|
||||
|abstract class AbstractClass(val a: Int)
|
||||
|""".trimMargin())
|
||||
|
||||
val result = call.execute()
|
||||
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
|
||||
assertThat(result.systemErr).contains(
|
||||
"error: @JsonClass can't be applied to AbstractClass: must not be abstract")
|
||||
}
|
||||
|
||||
@Test fun innerClassesNotSupported() {
|
||||
val call = KotlinCompilerCall(temporaryFolder.root)
|
||||
call.inheritClasspath = true
|
||||
call.addService(Processor::class, JsonClassCodegenProcessor::class)
|
||||
call.addKt("source.kt", """
|
||||
|import com.squareup.moshi.JsonClass
|
||||
|
|
||||
|class Outer {
|
||||
| @JsonClass(generateAdapter = true)
|
||||
| inner class InnerClass(val a: Int)
|
||||
|}
|
||||
|""".trimMargin())
|
||||
|
||||
val result = call.execute()
|
||||
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
|
||||
assertThat(result.systemErr).contains(
|
||||
"error: @JsonClass can't be applied to Outer.InnerClass: must not be an inner class")
|
||||
}
|
||||
|
||||
@Test fun enumClassesNotSupported() {
|
||||
val call = KotlinCompilerCall(temporaryFolder.root)
|
||||
call.inheritClasspath = true
|
||||
call.addService(Processor::class, JsonClassCodegenProcessor::class)
|
||||
call.addKt("source.kt", """
|
||||
|import com.squareup.moshi.JsonClass
|
||||
|
|
||||
|@JsonClass(generateAdapter = true)
|
||||
|enum class KotlinEnum {
|
||||
| A, B
|
||||
|}
|
||||
|""".trimMargin())
|
||||
|
||||
val result = call.execute()
|
||||
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
|
||||
assertThat(result.systemErr).contains(
|
||||
"error: @JsonClass with 'generateAdapter = \"true\"' can't be applied to KotlinEnum: code gen for enums is not supported or necessary")
|
||||
}
|
||||
|
||||
// Annotation processors don't get called for local classes, so we don't have the opportunity to
|
||||
// print an error message. Instead local classes will fail at runtime.
|
||||
@Ignore
|
||||
@Test fun localClassesNotSupported() {
|
||||
val call = KotlinCompilerCall(temporaryFolder.root)
|
||||
call.inheritClasspath = true
|
||||
call.addService(Processor::class, JsonClassCodegenProcessor::class)
|
||||
call.addKt("source.kt", """
|
||||
|import com.squareup.moshi.JsonClass
|
||||
|
|
||||
|fun outer() {
|
||||
| @JsonClass(generateAdapter = true)
|
||||
| class LocalClass(val a: Int)
|
||||
|}
|
||||
|""".trimMargin())
|
||||
|
||||
val result = call.execute()
|
||||
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
|
||||
assertThat(result.systemErr).contains(
|
||||
"error: @JsonClass can't be applied to LocalClass: must not be local")
|
||||
}
|
||||
|
||||
@Test fun objectDeclarationsNotSupported() {
|
||||
val call = KotlinCompilerCall(temporaryFolder.root)
|
||||
call.inheritClasspath = true
|
||||
call.addService(Processor::class, JsonClassCodegenProcessor::class)
|
||||
call.addKt("source.kt", """
|
||||
|import com.squareup.moshi.JsonClass
|
||||
|
|
||||
|@JsonClass(generateAdapter = true)
|
||||
|object ObjectDeclaration {
|
||||
| var a = 5
|
||||
|}
|
||||
|""".trimMargin())
|
||||
val result = call.execute()
|
||||
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
|
||||
assertThat(result.systemErr).contains(
|
||||
"error: @JsonClass can't be applied to ObjectDeclaration: must be a Kotlin class")
|
||||
}
|
||||
|
||||
@Test fun objectExpressionsNotSupported() {
|
||||
val call = KotlinCompilerCall(temporaryFolder.root)
|
||||
call.inheritClasspath = true
|
||||
call.addService(Processor::class, JsonClassCodegenProcessor::class)
|
||||
call.addKt("source.kt", """
|
||||
|import com.squareup.moshi.JsonClass
|
||||
|
|
||||
|@JsonClass(generateAdapter = true)
|
||||
|val expression = object : Any() {
|
||||
| var a = 5
|
||||
|}
|
||||
|""".trimMargin())
|
||||
|
||||
val result = call.execute()
|
||||
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
|
||||
assertThat(result.systemErr).contains(
|
||||
"error: @JsonClass can't be applied to expression\$annotations(): must be a Kotlin class")
|
||||
}
|
||||
|
||||
@Test fun requiredTransientConstructorParameterFails() {
|
||||
val call = KotlinCompilerCall(temporaryFolder.root)
|
||||
call.inheritClasspath = true
|
||||
call.addService(Processor::class, JsonClassCodegenProcessor::class)
|
||||
call.addKt("source.kt", """
|
||||
|import com.squareup.moshi.JsonClass
|
||||
|
|
||||
|@JsonClass(generateAdapter = true)
|
||||
|class RequiredTransientConstructorParameter(@Transient var a: Int)
|
||||
|""".trimMargin())
|
||||
|
||||
val result = call.execute()
|
||||
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
|
||||
assertThat(result.systemErr).contains(
|
||||
"error: No default value for transient property a")
|
||||
}
|
||||
|
||||
@Test fun nonPropertyConstructorParameter() {
|
||||
val call = KotlinCompilerCall(temporaryFolder.root)
|
||||
call.inheritClasspath = true
|
||||
call.addService(Processor::class, JsonClassCodegenProcessor::class)
|
||||
call.addKt("source.kt", """
|
||||
|import com.squareup.moshi.JsonClass
|
||||
|
|
||||
|@JsonClass(generateAdapter = true)
|
||||
|class NonPropertyConstructorParameter(a: Int, val b: Int)
|
||||
|""".trimMargin())
|
||||
|
||||
val result = call.execute()
|
||||
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
|
||||
assertThat(result.systemErr).contains(
|
||||
"error: No property for required constructor parameter a")
|
||||
}
|
||||
|
||||
@Test fun badGeneratedAnnotation() {
|
||||
val call = KotlinCompilerCall(temporaryFolder.root)
|
||||
call.inheritClasspath = true
|
||||
call.addService(Processor::class, JsonClassCodegenProcessor::class)
|
||||
call.kaptArgs[JsonClassCodegenProcessor.OPTION_GENERATED] = "javax.annotation.GeneratedBlerg"
|
||||
call.addKt("source.kt", """
|
||||
|import com.squareup.moshi.JsonClass
|
||||
|
|
||||
|@JsonClass(generateAdapter = true)
|
||||
|data class Foo(val a: Int)
|
||||
|""".trimMargin())
|
||||
|
||||
val result = call.execute()
|
||||
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
|
||||
assertThat(result.systemErr).contains(
|
||||
"Invalid option value for ${JsonClassCodegenProcessor.OPTION_GENERATED}")
|
||||
}
|
||||
|
||||
@Test fun multipleErrors() {
|
||||
val call = KotlinCompilerCall(temporaryFolder.root)
|
||||
call.inheritClasspath = true
|
||||
call.addService(Processor::class, JsonClassCodegenProcessor::class)
|
||||
call.addKt("source.kt", """
|
||||
|import com.squareup.moshi.JsonClass
|
||||
|
|
||||
|@JsonClass(generateAdapter = true)
|
||||
|class Class1(private var a: Int, private var b: Int)
|
||||
|
|
||||
|@JsonClass(generateAdapter = true)
|
||||
|class Class2(private var c: Int)
|
||||
|""".trimMargin())
|
||||
|
||||
val result = call.execute()
|
||||
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
|
||||
assertThat(result.systemErr).contains("property a is not visible")
|
||||
assertThat(result.systemErr).contains("property b is not visible")
|
||||
assertThat(result.systemErr).contains("property c is not visible")
|
||||
}
|
||||
|
||||
@Test fun extendPlatformType() {
|
||||
val call = KotlinCompilerCall(temporaryFolder.root)
|
||||
call.inheritClasspath = true
|
||||
call.addService(Processor::class, JsonClassCodegenProcessor::class)
|
||||
call.addKt("source.kt", """
|
||||
|import com.squareup.moshi.JsonClass
|
||||
|import java.util.Date
|
||||
|
|
||||
|@JsonClass(generateAdapter = true)
|
||||
|class ExtendsPlatformClass(var a: Int) : Date()
|
||||
|""".trimMargin())
|
||||
|
||||
val result = call.execute()
|
||||
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
|
||||
assertThat(result.systemErr).contains("supertype java.util.Date is not a Kotlin type")
|
||||
}
|
||||
|
||||
@Test fun extendJavaType() {
|
||||
val call = KotlinCompilerCall(temporaryFolder.root)
|
||||
call.inheritClasspath = true
|
||||
call.addService(Processor::class, JsonClassCodegenProcessor::class)
|
||||
call.addKt("source.kt", """
|
||||
|import com.squareup.moshi.JsonClass
|
||||
|import com.squareup.moshi.kotlin.codegen.JavaSuperclass
|
||||
|
|
||||
|@JsonClass(generateAdapter = true)
|
||||
|class ExtendsJavaType(var b: Int) : JavaSuperclass()
|
||||
|""".trimMargin())
|
||||
|
||||
val result = call.execute()
|
||||
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
|
||||
assertThat(result.systemErr)
|
||||
.contains("supertype com.squareup.moshi.kotlin.codegen.JavaSuperclass is not a Kotlin type")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonFieldApplicableQualifier() {
|
||||
val call = KotlinCompilerCall(temporaryFolder.root)
|
||||
call.inheritClasspath = true
|
||||
call.addService(Processor::class, JsonClassCodegenProcessor::class)
|
||||
call.addKt("source.kt", """
|
||||
|import com.squareup.moshi.JsonClass
|
||||
|import com.squareup.moshi.JsonQualifier
|
||||
|import kotlin.annotation.AnnotationRetention.RUNTIME
|
||||
|import kotlin.annotation.AnnotationTarget.PROPERTY
|
||||
|import kotlin.annotation.Retention
|
||||
|import kotlin.annotation.Target
|
||||
|
|
||||
|@Retention(RUNTIME)
|
||||
|@Target(PROPERTY)
|
||||
|@JsonQualifier
|
||||
|annotation class UpperCase
|
||||
|
|
||||
|@JsonClass(generateAdapter = true)
|
||||
|class ClassWithQualifier(@UpperCase val a: Int)
|
||||
|""".trimMargin())
|
||||
|
||||
val result = call.execute()
|
||||
println(result.systemErr)
|
||||
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
|
||||
assertThat(result.systemErr).contains("JsonQualifier @UpperCase must support FIELD target")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonRuntimeQualifier() {
|
||||
val call = KotlinCompilerCall(temporaryFolder.root)
|
||||
call.inheritClasspath = true
|
||||
call.addService(Processor::class, JsonClassCodegenProcessor::class)
|
||||
call.addKt("source.kt", """
|
||||
|import com.squareup.moshi.JsonClass
|
||||
|import com.squareup.moshi.JsonQualifier
|
||||
|import kotlin.annotation.AnnotationRetention.BINARY
|
||||
|import kotlin.annotation.AnnotationTarget.FIELD
|
||||
|import kotlin.annotation.AnnotationTarget.PROPERTY
|
||||
|import kotlin.annotation.Retention
|
||||
|import kotlin.annotation.Target
|
||||
|
|
||||
|@Retention(BINARY)
|
||||
|@Target(PROPERTY, FIELD)
|
||||
|@JsonQualifier
|
||||
|annotation class UpperCase
|
||||
|
|
||||
|@JsonClass(generateAdapter = true)
|
||||
|class ClassWithQualifier(@UpperCase val a: Int)
|
||||
|""".trimMargin())
|
||||
|
||||
val result = call.execute()
|
||||
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
|
||||
assertThat(result.systemErr).contains("JsonQualifier @UpperCase must have RUNTIME retention")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.kotlin.codegen
|
||||
|
||||
import com.google.common.collect.LinkedHashMultimap
|
||||
import okio.Buffer
|
||||
import okio.Okio
|
||||
import org.jetbrains.kotlin.cli.common.CLITool
|
||||
import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.ObjectOutputStream
|
||||
import java.io.PrintStream
|
||||
import java.net.URLClassLoader
|
||||
import java.net.URLDecoder
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/** Prepares an invocation of the Kotlin compiler. */
|
||||
class KotlinCompilerCall(var scratchDir: File) {
|
||||
val sourcesDir = File(scratchDir, "sources")
|
||||
val classesDir = File(scratchDir, "classes")
|
||||
val servicesJar = File(scratchDir, "services.jar")
|
||||
|
||||
var inheritClasspath = false
|
||||
|
||||
val args = mutableListOf<String>()
|
||||
val kaptArgs = mutableMapOf<String, String>()
|
||||
val classpath = mutableListOf<String>()
|
||||
val services = LinkedHashMultimap.create<KClass<*>, KClass<*>>()
|
||||
|
||||
/** Adds a source file to be compiled. */
|
||||
fun addKt(path: String, source: String) {
|
||||
val sourceFile = File(sourcesDir, path)
|
||||
sourceFile.parentFile.mkdirs()
|
||||
Okio.buffer(Okio.sink(sourceFile)).use {
|
||||
it.writeUtf8(source)
|
||||
}
|
||||
}
|
||||
|
||||
/** Adds a service like an annotation processor to make available to the compiler. */
|
||||
fun addService(serviceClass: KClass<*>, implementation: KClass<*>) {
|
||||
services.put(serviceClass, implementation)
|
||||
}
|
||||
|
||||
fun execute(): KotlinCompilerResult {
|
||||
val fullArgs = mutableListOf<String>()
|
||||
fullArgs.addAll(args)
|
||||
|
||||
fullArgs.add("-d")
|
||||
fullArgs.add(classesDir.toString())
|
||||
|
||||
val fullClasspath = fullClasspath()
|
||||
if (fullClasspath.isNotEmpty()) {
|
||||
fullArgs.add("-classpath")
|
||||
fullArgs.add(fullClasspath.joinToString(separator = ":"))
|
||||
}
|
||||
|
||||
for (source in sourcesDir.listFiles()) {
|
||||
fullArgs.add(source.toString())
|
||||
}
|
||||
|
||||
fullArgs.addAll(annotationProcessorArgs())
|
||||
if (kaptArgs.isNotEmpty()) {
|
||||
fullArgs.apply {
|
||||
add("-P")
|
||||
add("plugin:org.jetbrains.kotlin.kapt3:apoptions=${encodeOptions(kaptArgs)}")
|
||||
}
|
||||
}
|
||||
|
||||
val systemErrBuffer = Buffer()
|
||||
val oldSystemErr = System.err
|
||||
System.setErr(PrintStream(systemErrBuffer.outputStream()))
|
||||
try {
|
||||
val exitCode = CLITool.doMainNoExit(K2JVMCompiler(), fullArgs.toTypedArray())
|
||||
val systemErr = systemErrBuffer.readUtf8()
|
||||
return KotlinCompilerResult(systemErr, exitCode)
|
||||
} finally {
|
||||
System.setErr(oldSystemErr)
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns arguments necessary to enable and configure kapt3. */
|
||||
private fun annotationProcessorArgs(): List<String> {
|
||||
val kaptSourceDir = File(scratchDir, "kapt/sources")
|
||||
val kaptStubsDir = File(scratchDir, "kapt/stubs")
|
||||
|
||||
return listOf(
|
||||
"-Xplugin=${kapt3Jar()}",
|
||||
"-P", "plugin:org.jetbrains.kotlin.kapt3:sources=$kaptSourceDir",
|
||||
"-P", "plugin:org.jetbrains.kotlin.kapt3:classes=$classesDir",
|
||||
"-P", "plugin:org.jetbrains.kotlin.kapt3:stubs=$kaptStubsDir",
|
||||
"-P", "plugin:org.jetbrains.kotlin.kapt3:apclasspath=$servicesJar",
|
||||
"-P", "plugin:org.jetbrains.kotlin.kapt3:correctErrorTypes=true"
|
||||
)
|
||||
}
|
||||
|
||||
/** Returns the classpath to use when compiling code. */
|
||||
private fun fullClasspath(): List<String> {
|
||||
val result = mutableListOf<String>()
|
||||
result.addAll(classpath)
|
||||
|
||||
// Copy over the classpath of the running application.
|
||||
if (inheritClasspath) {
|
||||
for (classpathFile in classpathFiles()) {
|
||||
result.add(classpathFile.toString())
|
||||
}
|
||||
}
|
||||
|
||||
if (!services.isEmpty) {
|
||||
writeServicesJar()
|
||||
result.add(servicesJar.toString())
|
||||
}
|
||||
|
||||
return result.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a .jar file that holds ServiceManager registrations. Necessary because AutoService's
|
||||
* results might not be visible to this test.
|
||||
*/
|
||||
private fun writeServicesJar() {
|
||||
ZipOutputStream(FileOutputStream(servicesJar)).use { zipOutputStream ->
|
||||
for (entry in services.asMap()) {
|
||||
zipOutputStream.putNextEntry(
|
||||
ZipEntry("META-INF/services/${entry.key.qualifiedName}"))
|
||||
val serviceFile = Okio.buffer(Okio.sink(zipOutputStream))
|
||||
for (implementation in entry.value) {
|
||||
serviceFile.writeUtf8(implementation.qualifiedName!!)
|
||||
serviceFile.writeUtf8("\n")
|
||||
}
|
||||
serviceFile.emit() // Don't close the entry; that closes the file.
|
||||
zipOutputStream.closeEntry()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the files on the host process' classpath. */
|
||||
private fun classpathFiles(): List<File> {
|
||||
val classLoader = JsonClassCodegenProcessorTest::class.java.classLoader
|
||||
if (classLoader !is URLClassLoader) {
|
||||
throw UnsupportedOperationException("unable to extract classpath from $classLoader")
|
||||
}
|
||||
|
||||
val result = mutableListOf<File>()
|
||||
for (url in classLoader.urLs) {
|
||||
if (url.protocol != "file") {
|
||||
throw UnsupportedOperationException("unable to handle classpath element $url")
|
||||
}
|
||||
result.add(File(URLDecoder.decode(url.path, "UTF-8")))
|
||||
}
|
||||
return result.toList()
|
||||
}
|
||||
|
||||
/** Returns the path to the kotlin-annotation-processing .jar file. */
|
||||
private fun kapt3Jar(): File {
|
||||
for (file in classpathFiles()) {
|
||||
if (file.name.startsWith("kotlin-annotation-processing-embeddable")) return file
|
||||
}
|
||||
throw IllegalStateException("no kotlin-annotation-processing-embeddable jar on classpath:\n " +
|
||||
"${classpathFiles().joinToString(separator = "\n ")}}")
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 encodes a mapping of annotation processor args for kapt, as specified by
|
||||
* https://kotlinlang.org/docs/reference/kapt.html#apjavac-options-encoding
|
||||
*/
|
||||
private fun encodeOptions(options: Map<String, String>): String {
|
||||
val buffer = Buffer()
|
||||
ObjectOutputStream(buffer.outputStream()).use { oos ->
|
||||
oos.writeInt(options.size)
|
||||
for ((key, value) in options.entries) {
|
||||
oos.writeUTF(key)
|
||||
oos.writeUTF(value)
|
||||
}
|
||||
}
|
||||
return buffer.readByteString().base64()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.kotlin.codegen
|
||||
|
||||
import org.jetbrains.kotlin.cli.common.ExitCode
|
||||
|
||||
class KotlinCompilerResult(
|
||||
val systemErr: String,
|
||||
var exitCode: ExitCode
|
||||
)
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.kotlin.codegen
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.plusParameter
|
||||
import com.squareup.kotlinpoet.WildcardTypeName
|
||||
import com.squareup.kotlinpoet.asClassName
|
||||
import org.junit.Test
|
||||
|
||||
class TypeResolverTest {
|
||||
private val resolver = TypeResolver()
|
||||
|
||||
@Test
|
||||
fun ensureClassNameNullabilityIsPreserved() {
|
||||
assertThat(resolver.resolve(Int::class.asClassName().copy(nullable = true)).isNullable).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ensureParameterizedNullabilityIsPreserved() {
|
||||
val nullableTypeName = List::class.plusParameter(String::class).copy(nullable = true)
|
||||
|
||||
assertThat(resolver.resolve(nullableTypeName).isNullable).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ensureWildcardNullabilityIsPreserved() {
|
||||
val nullableTypeName = WildcardTypeName.producerOf(List::class.asClassName())
|
||||
.copy(nullable = true)
|
||||
|
||||
assertThat(resolver.resolve(nullableTypeName).isNullable).isTrue()
|
||||
}
|
||||
}
|
137
kotlin/reflect/pom.xml
Normal file
137
kotlin/reflect/pom.xml
Normal file
|
@ -0,0 +1,137 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi-parent</artifactId>
|
||||
<version>1.9.0-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>moshi-kotlin</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-reflect</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<properties>
|
||||
<maven.javadoc.skip>true</maven.javadoc.skip><!-- We use Dokka instead. -->
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>test-compile</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals>
|
||||
<goal>test-compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>testCompile</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals>
|
||||
<goal>testCompile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifestEntries>
|
||||
<Automatic-Module-Name>com.squareup.moshi.kotlin</Automatic-Module-Name>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>${maven-assembly.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<descriptors>
|
||||
<descriptor>src/assembly/dokka.xml</descriptor>
|
||||
</descriptors>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.dokka</groupId>
|
||||
<artifactId>dokka-maven-plugin</artifactId>
|
||||
<version>${dokka.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>dokka</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
16
kotlin/reflect/src/assembly/dokka.xml
Normal file
16
kotlin/reflect/src/assembly/dokka.xml
Normal file
|
@ -0,0 +1,16 @@
|
|||
<assembly
|
||||
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
|
||||
<id>javadoc</id>
|
||||
<formats>
|
||||
<format>jar</format>
|
||||
</formats>
|
||||
<baseDirectory>/</baseDirectory>
|
||||
<fileSets>
|
||||
<fileSet>
|
||||
<directory>target/dokka/moshi-kotlin</directory>
|
||||
<outputDirectory>/</outputDirectory>
|
||||
</fileSet>
|
||||
</fileSets>
|
||||
</assembly>
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (C) 2017 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi
|
||||
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
|
||||
@Deprecated(
|
||||
message = "this moved to avoid a package name conflict in the Java Platform Module System.",
|
||||
replaceWith = ReplaceWith("com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory")
|
||||
)
|
||||
class KotlinJsonAdapterFactory
|
||||
: JsonAdapter.Factory by KotlinJsonAdapterFactory()
|
|
@ -0,0 +1,262 @@
|
|||
/*
|
||||
* Copyright (C) 2017 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.kotlin.reflect
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.squareup.moshi.internal.Util
|
||||
import com.squareup.moshi.internal.Util.generatedAdapter
|
||||
import com.squareup.moshi.internal.Util.resolve
|
||||
import java.lang.reflect.Modifier
|
||||
import java.lang.reflect.Type
|
||||
import java.util.AbstractMap.SimpleEntry
|
||||
import kotlin.collections.Map.Entry
|
||||
import kotlin.reflect.KFunction
|
||||
import kotlin.reflect.KMutableProperty1
|
||||
import kotlin.reflect.KParameter
|
||||
import kotlin.reflect.KProperty1
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
import kotlin.reflect.jvm.isAccessible
|
||||
import kotlin.reflect.jvm.javaField
|
||||
import kotlin.reflect.jvm.javaType
|
||||
|
||||
/** Classes annotated with this are eligible for this adapter. */
|
||||
private val KOTLIN_METADATA = Class.forName("kotlin.Metadata") as Class<out Annotation>
|
||||
|
||||
/**
|
||||
* Placeholder value used when a field is absent from the JSON. Note that this code
|
||||
* distinguishes between absent values and present-but-null values.
|
||||
*/
|
||||
private val ABSENT_VALUE = Any()
|
||||
|
||||
/**
|
||||
* This class encodes Kotlin classes using their properties. It decodes them by first invoking the
|
||||
* constructor, and then by setting any additional properties that exist, if any.
|
||||
*/
|
||||
internal class KotlinJsonAdapter<T>(
|
||||
val constructor: KFunction<T>,
|
||||
val bindings: List<Binding<T, Any?>?>,
|
||||
val options: JsonReader.Options) : JsonAdapter<T>() {
|
||||
|
||||
override fun fromJson(reader: JsonReader): T {
|
||||
val constructorSize = constructor.parameters.size
|
||||
|
||||
// Read each value into its slot in the array.
|
||||
val values = Array<Any?>(bindings.size) { ABSENT_VALUE }
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
val index = reader.selectName(options)
|
||||
val binding = if (index != -1) bindings[index] else null
|
||||
|
||||
if (binding == null) {
|
||||
reader.skipName()
|
||||
reader.skipValue()
|
||||
continue
|
||||
}
|
||||
|
||||
if (values[index] !== ABSENT_VALUE) {
|
||||
throw JsonDataException(
|
||||
"Multiple values for '${binding.property.name}' at ${reader.path}")
|
||||
}
|
||||
|
||||
values[index] = binding.adapter.fromJson(reader)
|
||||
|
||||
if (values[index] == null && !binding.property.returnType.isMarkedNullable) {
|
||||
throw JsonDataException(
|
||||
"Non-null value '${binding.property.name}' was null at ${reader.path}")
|
||||
}
|
||||
}
|
||||
reader.endObject()
|
||||
|
||||
// Confirm all parameters are present, optional, or nullable.
|
||||
for (i in 0 until constructorSize) {
|
||||
if (values[i] === ABSENT_VALUE && !constructor.parameters[i].isOptional) {
|
||||
if (!constructor.parameters[i].type.isMarkedNullable) {
|
||||
throw JsonDataException(
|
||||
"Required value '${constructor.parameters[i].name}' missing at ${reader.path}")
|
||||
}
|
||||
values[i] = null // Replace absent with null.
|
||||
}
|
||||
}
|
||||
|
||||
// Call the constructor using a Map so that absent optionals get defaults.
|
||||
val result = constructor.callBy(IndexedParameterMap(constructor.parameters, values))
|
||||
|
||||
// Set remaining properties.
|
||||
for (i in constructorSize until bindings.size) {
|
||||
val binding = bindings[i]!!
|
||||
val value = values[i]
|
||||
binding.set(result, value)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toJson(writer: JsonWriter, value: T?) {
|
||||
if (value == null) throw NullPointerException("value == null")
|
||||
|
||||
writer.beginObject()
|
||||
for (binding in bindings) {
|
||||
if (binding == null) continue // Skip constructor parameters that aren't properties.
|
||||
|
||||
writer.name(binding.name)
|
||||
binding.adapter.toJson(writer, binding.get(value))
|
||||
}
|
||||
writer.endObject()
|
||||
}
|
||||
|
||||
override fun toString() = "KotlinJsonAdapter(${constructor.returnType})"
|
||||
|
||||
data class Binding<K, P>(
|
||||
val name: String,
|
||||
val adapter: JsonAdapter<P>,
|
||||
val property: KProperty1<K, P>,
|
||||
val parameter: KParameter?) {
|
||||
fun get(value: K) = property.get(value)
|
||||
|
||||
fun set(result: K, value: P) {
|
||||
if (value !== ABSENT_VALUE) {
|
||||
(property as KMutableProperty1<K, P>).set(result, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A simple [Map] that uses parameter indexes instead of sorting or hashing. */
|
||||
class IndexedParameterMap(val parameterKeys: List<KParameter>, val parameterValues: Array<Any?>)
|
||||
: AbstractMap<KParameter, Any?>() {
|
||||
|
||||
override val entries: Set<Entry<KParameter, Any?>>
|
||||
get() {
|
||||
val allPossibleEntries = parameterKeys.mapIndexed { index, value ->
|
||||
SimpleEntry<KParameter, Any?>(value, parameterValues[index])
|
||||
}
|
||||
return allPossibleEntries.filterTo(LinkedHashSet<Entry<KParameter, Any?>>()) {
|
||||
it.value !== ABSENT_VALUE
|
||||
}
|
||||
}
|
||||
|
||||
override fun containsKey(key: KParameter) = parameterValues[key.index] !== ABSENT_VALUE
|
||||
|
||||
override fun get(key: KParameter): Any? {
|
||||
val value = parameterValues[key.index]
|
||||
return if (value !== ABSENT_VALUE) value else null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class KotlinJsonAdapterFactory : JsonAdapter.Factory {
|
||||
override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi)
|
||||
: JsonAdapter<*>? {
|
||||
if (!annotations.isEmpty()) return null
|
||||
|
||||
val rawType = Types.getRawType(type)
|
||||
if (rawType.isInterface) return null
|
||||
if (rawType.isEnum) return null
|
||||
if (!rawType.isAnnotationPresent(KOTLIN_METADATA)) return null
|
||||
if (Util.isPlatformType(rawType)) return null
|
||||
try {
|
||||
val generatedAdapter = generatedAdapter(moshi, type, rawType)
|
||||
if (generatedAdapter != null) {
|
||||
return generatedAdapter
|
||||
}
|
||||
} catch (e: RuntimeException) {
|
||||
if (e.cause !is ClassNotFoundException) {
|
||||
throw e
|
||||
}
|
||||
// Fall back to a reflective adapter when the generated adapter is not found.
|
||||
}
|
||||
|
||||
if (rawType.isLocalClass) {
|
||||
throw IllegalArgumentException("Cannot serialize local class or object expression ${rawType.name}")
|
||||
}
|
||||
val rawTypeKotlin = rawType.kotlin
|
||||
if (rawTypeKotlin.isAbstract) {
|
||||
throw IllegalArgumentException("Cannot serialize abstract class ${rawType.name}")
|
||||
}
|
||||
if (rawTypeKotlin.isInner) {
|
||||
throw IllegalArgumentException("Cannot serialize inner class ${rawType.name}")
|
||||
}
|
||||
if (rawTypeKotlin.objectInstance != null) {
|
||||
throw IllegalArgumentException("Cannot serialize object declaration ${rawType.name}")
|
||||
}
|
||||
|
||||
val constructor = rawTypeKotlin.primaryConstructor ?: return null
|
||||
val parametersByName = constructor.parameters.associateBy { it.name }
|
||||
constructor.isAccessible = true
|
||||
|
||||
val bindingsByName = LinkedHashMap<String, KotlinJsonAdapter.Binding<Any, Any?>>()
|
||||
|
||||
for (property in rawTypeKotlin.memberProperties) {
|
||||
val parameter = parametersByName[property.name]
|
||||
|
||||
if (Modifier.isTransient(property.javaField?.modifiers ?: 0)) {
|
||||
if (parameter != null && !parameter.isOptional) {
|
||||
throw IllegalArgumentException(
|
||||
"No default value for transient constructor $parameter")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (parameter != null && parameter.type != property.returnType) {
|
||||
throw IllegalArgumentException("'${property.name}' has a constructor parameter of type " +
|
||||
"${parameter.type} but a property of type ${property.returnType}.")
|
||||
}
|
||||
|
||||
if (property !is KMutableProperty1 && parameter == null) continue
|
||||
|
||||
property.isAccessible = true
|
||||
var allAnnotations = property.annotations
|
||||
var jsonAnnotation = property.findAnnotation<Json>()
|
||||
|
||||
if (parameter != null) {
|
||||
allAnnotations += parameter.annotations
|
||||
if (jsonAnnotation == null) {
|
||||
jsonAnnotation = parameter.findAnnotation<Json>()
|
||||
}
|
||||
}
|
||||
|
||||
val name = jsonAnnotation?.name ?: property.name
|
||||
val resolvedPropertyType = resolve(type, rawType, property.returnType.javaType)
|
||||
val adapter = moshi.adapter<Any>(
|
||||
resolvedPropertyType, Util.jsonAnnotations(allAnnotations.toTypedArray()), property.name)
|
||||
|
||||
bindingsByName[property.name] = KotlinJsonAdapter.Binding(name, adapter,
|
||||
property as KProperty1<Any, Any?>, parameter)
|
||||
}
|
||||
|
||||
val bindings = ArrayList<KotlinJsonAdapter.Binding<Any, Any?>?>()
|
||||
|
||||
for (parameter in constructor.parameters) {
|
||||
val binding = bindingsByName.remove(parameter.name)
|
||||
if (binding == null && !parameter.isOptional) {
|
||||
throw IllegalArgumentException("No property for required constructor ${parameter}")
|
||||
}
|
||||
bindings += binding
|
||||
}
|
||||
|
||||
bindings += bindingsByName.values
|
||||
|
||||
val options = JsonReader.Options.of(*bindings.map { it?.name ?: "\u0000" }.toTypedArray())
|
||||
return KotlinJsonAdapter(constructor, bindings, options).nullSafe()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
-keep class kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoaderImpl
|
||||
|
||||
-keepclassmembers class kotlin.Metadata {
|
||||
public <methods>;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package com.squareup.moshi.kotlin.reflect
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.Moshi
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
|
||||
class KotlinJsonAdapterTest {
|
||||
@JsonClass(generateAdapter = true)
|
||||
class Data
|
||||
|
||||
@Test fun fallsBackToReflectiveAdapterWithoutCodegen() {
|
||||
val moshi = Moshi.Builder()
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
val adapter = moshi.adapter(Data::class.java)
|
||||
assertThat(adapter.toString()).isEqualTo(
|
||||
"KotlinJsonAdapter(com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterTest.Data).nullSafe()"
|
||||
)
|
||||
}
|
||||
}
|
191
kotlin/tests/pom.xml
Normal file
191
kotlin/tests/pom.xml
Normal file
|
@ -0,0 +1,191 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi-parent</artifactId>
|
||||
<version>1.9.0-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>moshi-kotlin-tests</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi-kotlin</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<properties>
|
||||
<maven.javadoc.skip>true</maven.javadoc.skip><!-- We use Dokka instead. -->
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<version>${kotlin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>kapt</id>
|
||||
<goals>
|
||||
<goal>kapt</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<sourceDirs>
|
||||
<sourceDir>src/main/kotlin</sourceDir>
|
||||
</sourceDirs>
|
||||
<annotationProcessorPaths>
|
||||
<annotationProcessorPath>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi-kotlin-codegen</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</annotationProcessorPath>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<sourceDirs>
|
||||
<sourceDir>src/main/kotlin</sourceDir>
|
||||
<sourceDir>src/main/java</sourceDir>
|
||||
</sourceDirs>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>test-kapt</id>
|
||||
<goals>
|
||||
<goal>test-kapt</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<sourceDirs>
|
||||
<sourceDir>src/test/kotlin</sourceDir>
|
||||
<sourceDir>src/test/java</sourceDir>
|
||||
</sourceDirs>
|
||||
<annotationProcessorPaths>
|
||||
<annotationProcessorPath>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi-kotlin-codegen</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</annotationProcessorPath>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>test-compile</id>
|
||||
<goals>
|
||||
<goal>test-compile</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<sourceDirs>
|
||||
<sourceDir>src/test/kotlin</sourceDir>
|
||||
<sourceDir>src/test/java</sourceDir>
|
||||
<sourceDir>target/generated-sources/kapt/test</sourceDir>
|
||||
</sourceDirs>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<args>
|
||||
<arg>-Werror</arg>
|
||||
</args>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<configuration>
|
||||
<proc>none</proc>
|
||||
<source>1.6</source>
|
||||
<target>1.6</target>
|
||||
</configuration>
|
||||
<executions>
|
||||
<!-- Replacing default-compile as it is treated specially by maven -->
|
||||
<execution>
|
||||
<id>default-compile</id>
|
||||
<phase>none</phase>
|
||||
</execution>
|
||||
<!-- Replacing default-testCompile as it is treated specially by maven -->
|
||||
<execution>
|
||||
<id>default-testCompile</id>
|
||||
<phase>none</phase>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>java-compile</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>java-test-compile</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals>
|
||||
<goal>testCompile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>${maven-assembly.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<descriptors>
|
||||
<descriptor>src/assembly/dokka.xml</descriptor>
|
||||
</descriptors>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.dokka</groupId>
|
||||
<artifactId>dokka-maven-plugin</artifactId>
|
||||
<version>${dokka.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>dokka</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
16
kotlin/tests/src/assembly/dokka.xml
Normal file
16
kotlin/tests/src/assembly/dokka.xml
Normal file
|
@ -0,0 +1,16 @@
|
|||
<assembly
|
||||
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
|
||||
<id>javadoc</id>
|
||||
<formats>
|
||||
<format>jar</format>
|
||||
</formats>
|
||||
<baseDirectory>/</baseDirectory>
|
||||
<fileSets>
|
||||
<fileSet>
|
||||
<directory>target/dokka/moshi-kotlin-tests</directory>
|
||||
<outputDirectory>/</outputDirectory>
|
||||
</fileSet>
|
||||
</fileSets>
|
||||
</assembly>
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (C) 2019 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.kotlin.codgen
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import com.squareup.moshi.kotlin.codgen.GeneratedAdaptersTest.CustomGeneratedClass
|
||||
|
||||
// This also tests custom generated types with no moshi constructor
|
||||
class GeneratedAdaptersTest_CustomGeneratedClassJsonAdapter : JsonAdapter<CustomGeneratedClass>() {
|
||||
override fun fromJson(reader: JsonReader): CustomGeneratedClass? {
|
||||
TODO()
|
||||
}
|
||||
|
||||
override fun toJson(writer: JsonWriter, value: CustomGeneratedClass?) {
|
||||
TODO()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.squareup.moshi.kotlin.codgen.LooksLikeAClass
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* https://github.com/square/moshi/issues/783
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ClassInPackageThatLooksLikeAClass(val foo: String)
|
File diff suppressed because it is too large
Load diff
|
@ -6,7 +6,7 @@
|
|||
<parent>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi-parent</artifactId>
|
||||
<version>1.4.0</version>
|
||||
<version>1.9.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>moshi</artifactId>
|
||||
|
@ -17,6 +17,11 @@
|
|||
<groupId>com.squareup.okio</groupId>
|
||||
<artifactId>okio</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.findbugs</groupId>
|
||||
<artifactId>jsr305</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
|
@ -28,4 +33,31 @@
|
|||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifestEntries>
|
||||
<Automatic-Module-Name>com.squareup.moshi</Automatic-Module-Name>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>2.10.4</version>
|
||||
<configuration>
|
||||
<excludePackageNames>com.squareup.moshi.internal:com.squareup.moshi.internal.*</excludePackageNames>
|
||||
<links>
|
||||
<link>https://square.github.io/okio/</link>
|
||||
</links>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import com.squareup.moshi.internal.Util;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
@ -24,19 +25,22 @@ import java.lang.reflect.Type;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static com.squareup.moshi.Util.jsonAnnotations;
|
||||
import static com.squareup.moshi.internal.Util.canonicalize;
|
||||
import static com.squareup.moshi.internal.Util.jsonAnnotations;
|
||||
import static com.squareup.moshi.internal.Util.typeAnnotatedWithAnnotations;
|
||||
|
||||
final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
||||
private final List<AdapterMethod> toAdapters;
|
||||
private final List<AdapterMethod> fromAdapters;
|
||||
|
||||
public AdapterMethodsFactory(List<AdapterMethod> toAdapters, List<AdapterMethod> fromAdapters) {
|
||||
AdapterMethodsFactory(List<AdapterMethod> toAdapters, List<AdapterMethod> fromAdapters) {
|
||||
this.toAdapters = toAdapters;
|
||||
this.fromAdapters = fromAdapters;
|
||||
}
|
||||
|
||||
@Override public JsonAdapter<?> create(
|
||||
@Override public @Nullable JsonAdapter<?> create(
|
||||
final Type type, final Set<? extends Annotation> annotations, final Moshi moshi) {
|
||||
final AdapterMethod toAdapter = get(toAdapters, type, annotations);
|
||||
final AdapterMethod fromAdapter = get(fromAdapters, type, annotations);
|
||||
|
@ -49,17 +53,17 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
|||
} catch (IllegalArgumentException e) {
|
||||
String missingAnnotation = toAdapter == null ? "@ToJson" : "@FromJson";
|
||||
throw new IllegalArgumentException("No " + missingAnnotation + " adapter for "
|
||||
+ type + " annotated " + annotations);
|
||||
+ typeAnnotatedWithAnnotations(type, annotations), e);
|
||||
}
|
||||
} else {
|
||||
delegate = null;
|
||||
}
|
||||
|
||||
if (toAdapter != null) toAdapter.bind(moshi);
|
||||
if (fromAdapter != null) fromAdapter.bind(moshi);
|
||||
if (toAdapter != null) toAdapter.bind(moshi, this);
|
||||
if (fromAdapter != null) fromAdapter.bind(moshi, this);
|
||||
|
||||
return new JsonAdapter<Object>() {
|
||||
@Override public void toJson(JsonWriter writer, Object value) throws IOException {
|
||||
@Override public void toJson(JsonWriter writer, @Nullable Object value) throws IOException {
|
||||
if (toAdapter == null) {
|
||||
delegate.toJson(writer, value);
|
||||
} else if (!toAdapter.nullable && value == null) {
|
||||
|
@ -75,7 +79,7 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
|||
}
|
||||
}
|
||||
|
||||
@Override public Object fromJson(JsonReader reader) throws IOException {
|
||||
@Override public @Nullable Object fromJson(JsonReader reader) throws IOException {
|
||||
if (fromAdapter == null) {
|
||||
return delegate.fromJson(reader);
|
||||
} else if (!fromAdapter.nullable && reader.peek() == JsonReader.Token.NULL) {
|
||||
|
@ -155,7 +159,7 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
|||
Set<? extends Annotation> qualifierAnnotations = jsonAnnotations(parameterAnnotations[1]);
|
||||
return new AdapterMethod(parameterTypes[1], qualifierAnnotations, adapter, method,
|
||||
parameterTypes.length, 2, true) {
|
||||
@Override public void toJson(Moshi moshi, JsonWriter writer, Object value)
|
||||
@Override public void toJson(Moshi moshi, JsonWriter writer, @Nullable Object value)
|
||||
throws IOException, InvocationTargetException {
|
||||
invoke(writer, value);
|
||||
}
|
||||
|
@ -164,18 +168,22 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
|||
} else if (parameterTypes.length == 1 && returnType != void.class) {
|
||||
// List<Integer> pointToJson(Point point) {
|
||||
final Set<? extends Annotation> returnTypeAnnotations = jsonAnnotations(method);
|
||||
Set<? extends Annotation> qualifierAnnotations = jsonAnnotations(parameterAnnotations[0]);
|
||||
final Set<? extends Annotation> qualifierAnnotations =
|
||||
jsonAnnotations(parameterAnnotations[0]);
|
||||
boolean nullable = Util.hasNullable(parameterAnnotations[0]);
|
||||
return new AdapterMethod(parameterTypes[0], qualifierAnnotations, adapter, method,
|
||||
parameterTypes.length, 1, nullable) {
|
||||
private JsonAdapter<Object> delegate;
|
||||
|
||||
@Override public void bind(Moshi moshi) {
|
||||
super.bind(moshi);
|
||||
delegate = moshi.adapter(returnType, returnTypeAnnotations);
|
||||
@Override public void bind(Moshi moshi, JsonAdapter.Factory factory) {
|
||||
super.bind(moshi, factory);
|
||||
delegate = Types.equals(parameterTypes[0], returnType)
|
||||
&& qualifierAnnotations.equals(returnTypeAnnotations)
|
||||
? moshi.nextAdapter(factory, returnType, returnTypeAnnotations)
|
||||
: moshi.adapter(returnType, returnTypeAnnotations);
|
||||
}
|
||||
|
||||
@Override public void toJson(Moshi moshi, JsonWriter writer, Object value)
|
||||
@Override public void toJson(Moshi moshi, JsonWriter writer, @Nullable Object value)
|
||||
throws IOException, InvocationTargetException {
|
||||
Object intermediate = invoke(value);
|
||||
delegate.toJson(writer, intermediate);
|
||||
|
@ -186,6 +194,8 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
|||
throw new IllegalArgumentException("Unexpected signature for " + method + ".\n"
|
||||
+ "@ToJson method signatures may have one of the following structures:\n"
|
||||
+ " <any access modifier> void toJson(JsonWriter writer, T value) throws <any>;\n"
|
||||
+ " <any access modifier> void toJson(JsonWriter writer, T value,"
|
||||
+ " JsonAdapter<any> delegate, <any more delegates>) throws <any>;\n"
|
||||
+ " <any access modifier> R toJson(T value) throws <any>;\n");
|
||||
}
|
||||
}
|
||||
|
@ -206,7 +216,7 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
|||
static AdapterMethod fromAdapter(Object adapter, Method method) {
|
||||
method.setAccessible(true);
|
||||
final Type returnType = method.getGenericReturnType();
|
||||
Set<? extends Annotation> returnTypeAnnotations = jsonAnnotations(method);
|
||||
final Set<? extends Annotation> returnTypeAnnotations = jsonAnnotations(method);
|
||||
final Type[] parameterTypes = method.getGenericParameterTypes();
|
||||
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
|
||||
|
||||
|
@ -233,9 +243,12 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
|||
parameterTypes.length, 1, nullable) {
|
||||
JsonAdapter<Object> delegate;
|
||||
|
||||
@Override public void bind(Moshi moshi) {
|
||||
super.bind(moshi);
|
||||
delegate = moshi.adapter(parameterTypes[0], qualifierAnnotations);
|
||||
@Override public void bind(Moshi moshi, JsonAdapter.Factory factory) {
|
||||
super.bind(moshi, factory);
|
||||
delegate = Types.equals(parameterTypes[0], returnType)
|
||||
&& qualifierAnnotations.equals(returnTypeAnnotations)
|
||||
? moshi.nextAdapter(factory, parameterTypes[0], qualifierAnnotations)
|
||||
: moshi.adapter(parameterTypes[0], qualifierAnnotations);
|
||||
}
|
||||
|
||||
@Override public Object fromJson(Moshi moshi, JsonReader reader)
|
||||
|
@ -248,24 +261,26 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
|||
} else {
|
||||
throw new IllegalArgumentException("Unexpected signature for " + method + ".\n"
|
||||
+ "@FromJson method signatures may have one of the following structures:\n"
|
||||
+ " <any access modifier> void fromJson(JsonReader jsonReader) throws <any>;\n"
|
||||
+ " <any access modifier> R fromJson(JsonReader jsonReader) throws <any>;\n"
|
||||
+ " <any access modifier> R fromJson(JsonReader jsonReader,"
|
||||
+ " JsonAdapter<any> delegate, <any more delegates>) throws <any>;\n"
|
||||
+ " <any access modifier> R fromJson(T value) throws <any>;\n");
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the matching adapter method from the list. */
|
||||
private static AdapterMethod get(
|
||||
private static @Nullable AdapterMethod get(
|
||||
List<AdapterMethod> adapterMethods, Type type, Set<? extends Annotation> annotations) {
|
||||
for (int i = 0, size = adapterMethods.size(); i < size; i++) {
|
||||
AdapterMethod adapterMethod = adapterMethods.get(i);
|
||||
if (adapterMethod.type.equals(type) && adapterMethod.annotations.equals(annotations)) {
|
||||
if (Types.equals(adapterMethod.type, type) && adapterMethod.annotations.equals(annotations)) {
|
||||
return adapterMethod;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static abstract class AdapterMethod {
|
||||
abstract static class AdapterMethod {
|
||||
final Type type;
|
||||
final Set<? extends Annotation> annotations;
|
||||
final Object adapter;
|
||||
|
@ -274,9 +289,9 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
|||
final JsonAdapter<?>[] jsonAdapters;
|
||||
final boolean nullable;
|
||||
|
||||
public AdapterMethod(Type type, Set<? extends Annotation> annotations, Object adapter,
|
||||
AdapterMethod(Type type, Set<? extends Annotation> annotations, Object adapter,
|
||||
Method method, int parameterCount, int adaptersOffset, boolean nullable) {
|
||||
this.type = Types.canonicalize(type);
|
||||
this.type = canonicalize(type);
|
||||
this.annotations = annotations;
|
||||
this.adapter = adapter;
|
||||
this.method = method;
|
||||
|
@ -285,30 +300,33 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
|||
this.nullable = nullable;
|
||||
}
|
||||
|
||||
public void bind(Moshi moshi) {
|
||||
public void bind(Moshi moshi, JsonAdapter.Factory factory) {
|
||||
if (jsonAdapters.length > 0) {
|
||||
Type[] parameterTypes = method.getGenericParameterTypes();
|
||||
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
|
||||
for (int i = adaptersOffset, size = parameterTypes.length; i < size; i++) {
|
||||
jsonAdapters[i - adaptersOffset] = moshi.adapter(
|
||||
((ParameterizedType) parameterTypes[i]).getActualTypeArguments()[0],
|
||||
jsonAnnotations(parameterAnnotations[i]));
|
||||
Type type = ((ParameterizedType) parameterTypes[i]).getActualTypeArguments()[0];
|
||||
Set<? extends Annotation> jsonAnnotations = jsonAnnotations(parameterAnnotations[i]);
|
||||
jsonAdapters[i - adaptersOffset] =
|
||||
Types.equals(this.type, type) && annotations.equals(jsonAnnotations)
|
||||
? moshi.nextAdapter(factory, type, jsonAnnotations)
|
||||
: moshi.adapter(type, jsonAnnotations);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void toJson(Moshi moshi, JsonWriter writer, Object value)
|
||||
public void toJson(Moshi moshi, JsonWriter writer, @Nullable Object value)
|
||||
throws IOException, InvocationTargetException {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
public Object fromJson(Moshi moshi, JsonReader reader)
|
||||
public @Nullable Object fromJson(Moshi moshi, JsonReader reader)
|
||||
throws IOException, InvocationTargetException {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
/** Invoke the method with one fixed argument, plus any number of JSON adapter arguments. */
|
||||
protected Object invoke(Object a1) throws InvocationTargetException {
|
||||
protected @Nullable Object invoke(@Nullable Object a1) throws InvocationTargetException {
|
||||
Object[] args = new Object[1 + jsonAdapters.length];
|
||||
args[0] = a1;
|
||||
System.arraycopy(jsonAdapters, 0, args, 1, jsonAdapters.length);
|
||||
|
@ -321,7 +339,8 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
|||
}
|
||||
|
||||
/** Invoke the method with two fixed arguments, plus any number of JSON adapter arguments. */
|
||||
protected Object invoke(Object a1, Object a2) throws InvocationTargetException {
|
||||
protected Object invoke(@Nullable Object a1, @Nullable Object a2)
|
||||
throws InvocationTargetException {
|
||||
Object[] args = new Object[2 + jsonAdapters.length];
|
||||
args[0] = a1;
|
||||
args[1] = a2;
|
||||
|
|
|
@ -22,6 +22,7 @@ import java.lang.reflect.Type;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Converts arrays to JSON arrays containing their converted contents. This
|
||||
|
@ -29,7 +30,7 @@ import java.util.Set;
|
|||
*/
|
||||
final class ArrayJsonAdapter extends JsonAdapter<Object> {
|
||||
public static final Factory FACTORY = new Factory() {
|
||||
@Override public JsonAdapter<?> create(
|
||||
@Override public @Nullable JsonAdapter<?> create(
|
||||
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
Type elementType = Types.arrayComponentType(type);
|
||||
if (elementType == null) return null;
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import com.squareup.moshi.internal.Util;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectStreamClass;
|
||||
import java.lang.reflect.Constructor;
|
||||
|
@ -103,7 +104,7 @@ abstract class ClassFactory<T> {
|
|||
} catch (IllegalAccessException e) {
|
||||
throw new AssertionError();
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
throw Util.rethrowCause(e);
|
||||
} catch (NoSuchMethodException ignored) {
|
||||
// Not the expected version of Dalvik/libcore!
|
||||
}
|
||||
|
|
|
@ -15,15 +15,20 @@
|
|||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import com.squareup.moshi.internal.Util;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static com.squareup.moshi.internal.Util.resolve;
|
||||
|
||||
/**
|
||||
* Emits a regular class as a JSON object by mapping Java fields to JSON object properties.
|
||||
|
@ -42,31 +47,45 @@ import java.util.TreeMap;
|
|||
*/
|
||||
final class ClassJsonAdapter<T> extends JsonAdapter<T> {
|
||||
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
|
||||
@Override public JsonAdapter<?> create(
|
||||
@Override public @Nullable JsonAdapter<?> create(
|
||||
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
if (!(type instanceof Class) && !(type instanceof ParameterizedType)) {
|
||||
return null;
|
||||
}
|
||||
Class<?> rawType = Types.getRawType(type);
|
||||
if (rawType.isInterface() || rawType.isEnum()) return null;
|
||||
if (isPlatformType(rawType) && !Types.isAllowedPlatformType(rawType)) {
|
||||
throw new IllegalArgumentException("Platform "
|
||||
+ type
|
||||
+ " annotated "
|
||||
+ annotations
|
||||
+ " requires explicit JsonAdapter to be registered");
|
||||
}
|
||||
if (!annotations.isEmpty()) return null;
|
||||
if (Util.isPlatformType(rawType)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Platform " + type + " requires explicit JsonAdapter to be registered");
|
||||
}
|
||||
|
||||
if (rawType.isAnonymousClass()) {
|
||||
throw new IllegalArgumentException("Cannot serialize anonymous class " + rawType.getName());
|
||||
}
|
||||
if (rawType.isLocalClass()) {
|
||||
throw new IllegalArgumentException("Cannot serialize local class " + rawType.getName());
|
||||
}
|
||||
if (rawType.getEnclosingClass() != null && !Modifier.isStatic(rawType.getModifiers())) {
|
||||
if (rawType.getSimpleName().isEmpty()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot serialize anonymous class " + rawType.getName());
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot serialize non-static nested class " + rawType.getName());
|
||||
}
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot serialize non-static nested class " + rawType.getName());
|
||||
}
|
||||
if (Modifier.isAbstract(rawType.getModifiers())) {
|
||||
throw new IllegalArgumentException("Cannot serialize abstract class " + rawType.getName());
|
||||
}
|
||||
try {
|
||||
//noinspection unchecked if the Class.forName works, the cast will work.
|
||||
Class<? extends Annotation> metadataClass =
|
||||
(Class<? extends Annotation>) Class.forName("kotlin.Metadata");
|
||||
if (rawType.isAnnotationPresent(metadataClass)) {
|
||||
throw new IllegalArgumentException("Cannot serialize Kotlin type " + rawType.getName()
|
||||
+ ". Reflective serialization of Kotlin classes without using kotlin-reflect has "
|
||||
+ "undefined and unexpected behavior. Please use KotlinJsonAdapter from the "
|
||||
+ "moshi-kotlin artifact or use code gen from the moshi-kotlin-codegen artifact.");
|
||||
}
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
|
||||
}
|
||||
|
||||
ClassFactory<Object> classFactory = ClassFactory.get(rawType);
|
||||
Map<String, FieldBinding<?>> fields = new TreeMap<>();
|
||||
|
@ -80,21 +99,22 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
|
|||
private void createFieldBindings(
|
||||
Moshi moshi, Type type, Map<String, FieldBinding<?>> fieldBindings) {
|
||||
Class<?> rawType = Types.getRawType(type);
|
||||
boolean platformType = isPlatformType(rawType);
|
||||
boolean platformType = Util.isPlatformType(rawType);
|
||||
for (Field field : rawType.getDeclaredFields()) {
|
||||
if (!includeField(platformType, field.getModifiers())) continue;
|
||||
|
||||
// Look up a type adapter for this type.
|
||||
Type fieldType = Types.resolve(type, rawType, field.getGenericType());
|
||||
Type fieldType = resolve(type, rawType, field.getGenericType());
|
||||
Set<? extends Annotation> annotations = Util.jsonAnnotations(field);
|
||||
JsonAdapter<Object> adapter = moshi.adapter(fieldType, annotations);
|
||||
String fieldName = field.getName();
|
||||
JsonAdapter<Object> adapter = moshi.adapter(fieldType, annotations, fieldName);
|
||||
|
||||
// Create the binding between field and JSON.
|
||||
field.setAccessible(true);
|
||||
|
||||
// Store it using the field's name. If there was already a field with this name, fail!
|
||||
Json jsonAnnotation = field.getAnnotation(Json.class);
|
||||
String name = jsonAnnotation != null ? jsonAnnotation.name() : field.getName();
|
||||
String name = jsonAnnotation != null ? jsonAnnotation.name() : fieldName;
|
||||
FieldBinding<Object> fieldBinding = new FieldBinding<>(name, field, adapter);
|
||||
FieldBinding<?> replaced = fieldBindings.put(name, fieldBinding);
|
||||
if (replaced != null) {
|
||||
|
@ -105,19 +125,6 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if {@code rawType} is built in. We don't reflect on private fields of platform
|
||||
* types because they're unspecified and likely to be different on Java vs. Android.
|
||||
*/
|
||||
private boolean isPlatformType(Class<?> rawType) {
|
||||
String name = rawType.getName();
|
||||
return name.startsWith("android.")
|
||||
|| name.startsWith("java.")
|
||||
|| name.startsWith("javax.")
|
||||
|| name.startsWith("kotlin.")
|
||||
|| name.startsWith("scala.");
|
||||
}
|
||||
|
||||
/** Returns true if fields with {@code modifiers} are included in the emitted JSON. */
|
||||
private boolean includeField(boolean platformType, int modifiers) {
|
||||
if (Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers)) return false;
|
||||
|
@ -143,10 +150,7 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
|
|||
} catch (InstantiationException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (InvocationTargetException e) {
|
||||
Throwable targetException = e.getTargetException();
|
||||
if (targetException instanceof RuntimeException) throw (RuntimeException) targetException;
|
||||
if (targetException instanceof Error) throw (Error) targetException;
|
||||
throw new RuntimeException(targetException);
|
||||
throw Util.rethrowCause(e);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
@ -155,15 +159,12 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
|
|||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
int index = reader.selectName(options);
|
||||
FieldBinding<?> fieldBinding;
|
||||
if (index != -1) {
|
||||
fieldBinding = fieldsArray[index];
|
||||
} else {
|
||||
reader.nextName();
|
||||
if (index == -1) {
|
||||
reader.skipName();
|
||||
reader.skipValue();
|
||||
continue;
|
||||
}
|
||||
fieldBinding.read(reader, result);
|
||||
fieldsArray[index].read(reader, result);
|
||||
}
|
||||
reader.endObject();
|
||||
return result;
|
||||
|
@ -194,7 +195,7 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
|
|||
final Field field;
|
||||
final JsonAdapter<T> adapter;
|
||||
|
||||
public FieldBinding(String name, Field field, JsonAdapter<T> adapter) {
|
||||
FieldBinding(String name, Field field, JsonAdapter<T> adapter) {
|
||||
this.name = name;
|
||||
this.field = field;
|
||||
this.adapter = adapter;
|
||||
|
|
|
@ -23,11 +23,12 @@ import java.util.Collection;
|
|||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** Converts collection types to JSON arrays containing their converted contents. */
|
||||
abstract class CollectionJsonAdapter<C extends Collection<T>, T> extends JsonAdapter<C> {
|
||||
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
|
||||
@Override public JsonAdapter<?> create(
|
||||
@Override public @Nullable JsonAdapter<?> create(
|
||||
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
Class<?> rawType = Types.getRawType(type);
|
||||
if (!annotations.isEmpty()) return null;
|
||||
|
|
|
@ -19,12 +19,23 @@ import java.lang.annotation.Documented;
|
|||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
import static java.lang.annotation.ElementType.METHOD;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
/** Customizes how a field is encoded as JSON. */
|
||||
@Target({FIELD, METHOD})
|
||||
/**
|
||||
* Customizes how a field is encoded as JSON.
|
||||
*
|
||||
* <p>Although this annotation doesn't declare a {@link Target}, it is only honored in the following
|
||||
* elements:
|
||||
*
|
||||
* <ul>
|
||||
* <li><strong>Java class fields</strong>
|
||||
* <li><strong>Kotlin properties</strong> for use with {@code moshi-kotlin}. This includes both
|
||||
* properties declared in the constructor and properties declared as members.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Users of the <a href="https://github.com/rharter/auto-value-moshi">AutoValue: Moshi
|
||||
* Extension</a> may also use this annotation on abstract getters.
|
||||
*/
|
||||
@Retention(RUNTIME)
|
||||
@Documented
|
||||
public @interface Json {
|
||||
|
|
|
@ -15,11 +15,14 @@
|
|||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import com.squareup.moshi.internal.NullSafeJsonAdapter;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Type;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Set;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
import javax.annotation.Nullable;
|
||||
import okio.Buffer;
|
||||
import okio.BufferedSink;
|
||||
import okio.BufferedSource;
|
||||
|
@ -28,24 +31,29 @@ import okio.BufferedSource;
|
|||
* Converts Java values to JSON, and JSON values to Java.
|
||||
*/
|
||||
public abstract class JsonAdapter<T> {
|
||||
public abstract T fromJson(JsonReader reader) throws IOException;
|
||||
@CheckReturnValue public abstract @Nullable T fromJson(JsonReader reader) throws IOException;
|
||||
|
||||
public final T fromJson(BufferedSource source) throws IOException {
|
||||
@CheckReturnValue public final @Nullable T fromJson(BufferedSource source) throws IOException {
|
||||
return fromJson(JsonReader.of(source));
|
||||
}
|
||||
|
||||
public final T fromJson(String string) throws IOException {
|
||||
return fromJson(new Buffer().writeUtf8(string));
|
||||
@CheckReturnValue public final @Nullable T fromJson(String string) throws IOException {
|
||||
JsonReader reader = JsonReader.of(new Buffer().writeUtf8(string));
|
||||
T result = fromJson(reader);
|
||||
if (!isLenient() && reader.peek() != JsonReader.Token.END_DOCUMENT) {
|
||||
throw new JsonDataException("JSON document was not fully consumed.");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public abstract void toJson(JsonWriter writer, T value) throws IOException;
|
||||
public abstract void toJson(JsonWriter writer, @Nullable T value) throws IOException;
|
||||
|
||||
public final void toJson(BufferedSink sink, T value) throws IOException {
|
||||
public final void toJson(BufferedSink sink, @Nullable T value) throws IOException {
|
||||
JsonWriter writer = JsonWriter.of(sink);
|
||||
toJson(writer, value);
|
||||
}
|
||||
|
||||
public final String toJson(T value) {
|
||||
@CheckReturnValue public final String toJson(@Nullable T value) {
|
||||
Buffer buffer = new Buffer();
|
||||
try {
|
||||
toJson(buffer, value);
|
||||
|
@ -58,14 +66,14 @@ public abstract class JsonAdapter<T> {
|
|||
/**
|
||||
* Encodes {@code value} as a Java value object comprised of maps, lists, strings, numbers,
|
||||
* booleans, and nulls.
|
||||
*
|
||||
*
|
||||
* <p>Values encoded using {@code value(double)} or {@code value(long)} are modeled with the
|
||||
* corresponding boxed type. Values encoded using {@code value(Number)} are modeled as a
|
||||
* {@link Long} for boxed integer types ({@link Byte}, {@link Short}, {@link Integer}, and {@link
|
||||
* Long}), as a {@link Double} for boxed floating point types ({@link Float} and {@link Double}),
|
||||
* and as a {@link BigDecimal} for all other types.
|
||||
*/
|
||||
public final Object toJsonValue(T value) {
|
||||
@CheckReturnValue public final @Nullable Object toJsonValue(@Nullable T value) {
|
||||
JsonValueWriter writer = new JsonValueWriter();
|
||||
try {
|
||||
toJson(writer, value);
|
||||
|
@ -79,7 +87,7 @@ public abstract class JsonAdapter<T> {
|
|||
* Decodes a Java value object from {@code value}, which must be comprised of maps, lists,
|
||||
* strings, numbers, booleans and nulls.
|
||||
*/
|
||||
public final T fromJsonValue(Object value) {
|
||||
@CheckReturnValue public final @Nullable T fromJsonValue(@Nullable Object value) {
|
||||
JsonValueReader reader = new JsonValueReader(value);
|
||||
try {
|
||||
return fromJson(reader);
|
||||
|
@ -92,13 +100,13 @@ public abstract class JsonAdapter<T> {
|
|||
* Returns a JSON adapter equal to this JSON adapter, but that serializes nulls when encoding
|
||||
* JSON.
|
||||
*/
|
||||
public final JsonAdapter<T> serializeNulls() {
|
||||
@CheckReturnValue public final JsonAdapter<T> serializeNulls() {
|
||||
final JsonAdapter<T> delegate = this;
|
||||
return new JsonAdapter<T>() {
|
||||
@Override public T fromJson(JsonReader reader) throws IOException {
|
||||
@Override public @Nullable T fromJson(JsonReader reader) throws IOException {
|
||||
return delegate.fromJson(reader);
|
||||
}
|
||||
@Override public void toJson(JsonWriter writer, T value) throws IOException {
|
||||
@Override public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
|
||||
boolean serializeNulls = writer.getSerializeNulls();
|
||||
writer.setSerializeNulls(true);
|
||||
try {
|
||||
|
@ -107,6 +115,9 @@ public abstract class JsonAdapter<T> {
|
|||
writer.setSerializeNulls(serializeNulls);
|
||||
}
|
||||
}
|
||||
@Override boolean isLenient() {
|
||||
return delegate.isLenient();
|
||||
}
|
||||
@Override public String toString() {
|
||||
return delegate + ".serializeNulls()";
|
||||
}
|
||||
|
@ -117,34 +128,48 @@ public abstract class JsonAdapter<T> {
|
|||
* Returns a JSON adapter equal to this JSON adapter, but with support for reading and writing
|
||||
* nulls.
|
||||
*/
|
||||
public final JsonAdapter<T> nullSafe() {
|
||||
@CheckReturnValue public final JsonAdapter<T> nullSafe() {
|
||||
return new NullSafeJsonAdapter<>(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a JSON adapter equal to this JSON adapter, but that refuses null values. If null is
|
||||
* read or written this will throw a {@link JsonDataException}.
|
||||
*
|
||||
* <p>Note that this adapter will not usually be invoked for absent values and so those must be
|
||||
* handled elsewhere. This should only be used to fail on explicit nulls.
|
||||
*/
|
||||
@CheckReturnValue public final JsonAdapter<T> nonNull() {
|
||||
final JsonAdapter<T> delegate = this;
|
||||
return new JsonAdapter<T>() {
|
||||
@Override public T fromJson(JsonReader reader) throws IOException {
|
||||
@Override public @Nullable T fromJson(JsonReader reader) throws IOException {
|
||||
if (reader.peek() == JsonReader.Token.NULL) {
|
||||
return reader.nextNull();
|
||||
throw new JsonDataException("Unexpected null at " + reader.getPath());
|
||||
} else {
|
||||
return delegate.fromJson(reader);
|
||||
}
|
||||
}
|
||||
@Override public void toJson(JsonWriter writer, T value) throws IOException {
|
||||
@Override public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
|
||||
if (value == null) {
|
||||
writer.nullValue();
|
||||
throw new JsonDataException("Unexpected null at " + writer.getPath());
|
||||
} else {
|
||||
delegate.toJson(writer, value);
|
||||
}
|
||||
}
|
||||
@Override boolean isLenient() {
|
||||
return delegate.isLenient();
|
||||
}
|
||||
@Override public String toString() {
|
||||
return delegate + ".nullSafe()";
|
||||
return delegate + ".nonNull()";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Returns a JSON adapter equal to this, but is lenient when reading and writing. */
|
||||
public final JsonAdapter<T> lenient() {
|
||||
@CheckReturnValue public final JsonAdapter<T> lenient() {
|
||||
final JsonAdapter<T> delegate = this;
|
||||
return new JsonAdapter<T>() {
|
||||
@Override public T fromJson(JsonReader reader) throws IOException {
|
||||
@Override public @Nullable T fromJson(JsonReader reader) throws IOException {
|
||||
boolean lenient = reader.isLenient();
|
||||
reader.setLenient(true);
|
||||
try {
|
||||
|
@ -153,7 +178,7 @@ public abstract class JsonAdapter<T> {
|
|||
reader.setLenient(lenient);
|
||||
}
|
||||
}
|
||||
@Override public void toJson(JsonWriter writer, T value) throws IOException {
|
||||
@Override public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
|
||||
boolean lenient = writer.isLenient();
|
||||
writer.setLenient(true);
|
||||
try {
|
||||
|
@ -162,6 +187,9 @@ public abstract class JsonAdapter<T> {
|
|||
writer.setLenient(lenient);
|
||||
}
|
||||
}
|
||||
@Override boolean isLenient() {
|
||||
return true;
|
||||
}
|
||||
@Override public String toString() {
|
||||
return delegate + ".lenient()";
|
||||
}
|
||||
|
@ -170,14 +198,14 @@ public abstract class JsonAdapter<T> {
|
|||
|
||||
/**
|
||||
* Returns a JSON adapter equal to this, but that throws a {@link JsonDataException} when
|
||||
* {@linkplain JsonReader#setFailOnUnknown(boolean) unknown values} are encountered. This
|
||||
* constraint applies to both the top-level message handled by this type adapter as well as to
|
||||
* nested messages.
|
||||
* {@linkplain JsonReader#setFailOnUnknown(boolean) unknown names and values} are encountered.
|
||||
* This constraint applies to both the top-level message handled by this type adapter as well as
|
||||
* to nested messages.
|
||||
*/
|
||||
public final JsonAdapter<T> failOnUnknown() {
|
||||
@CheckReturnValue public final JsonAdapter<T> failOnUnknown() {
|
||||
final JsonAdapter<T> delegate = this;
|
||||
return new JsonAdapter<T>() {
|
||||
@Override public T fromJson(JsonReader reader) throws IOException {
|
||||
@Override public @Nullable T fromJson(JsonReader reader) throws IOException {
|
||||
boolean skipForbidden = reader.failOnUnknown();
|
||||
reader.setFailOnUnknown(true);
|
||||
try {
|
||||
|
@ -186,9 +214,12 @@ public abstract class JsonAdapter<T> {
|
|||
reader.setFailOnUnknown(skipForbidden);
|
||||
}
|
||||
}
|
||||
@Override public void toJson(JsonWriter writer, T value) throws IOException {
|
||||
@Override public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
|
||||
delegate.toJson(writer, value);
|
||||
}
|
||||
@Override boolean isLenient() {
|
||||
return delegate.isLenient();
|
||||
}
|
||||
@Override public String toString() {
|
||||
return delegate + ".failOnUnknown()";
|
||||
}
|
||||
|
@ -203,13 +234,16 @@ public abstract class JsonAdapter<T> {
|
|||
*
|
||||
* @param indent a string containing only whitespace.
|
||||
*/
|
||||
public JsonAdapter<T> indent(final String indent) {
|
||||
@CheckReturnValue public JsonAdapter<T> indent(final String indent) {
|
||||
if (indent == null) {
|
||||
throw new NullPointerException("indent == null");
|
||||
}
|
||||
final JsonAdapter<T> delegate = this;
|
||||
return new JsonAdapter<T>() {
|
||||
@Override public T fromJson(JsonReader reader) throws IOException {
|
||||
@Override public @Nullable T fromJson(JsonReader reader) throws IOException {
|
||||
return delegate.fromJson(reader);
|
||||
}
|
||||
@Override public void toJson(JsonWriter writer, T value) throws IOException {
|
||||
@Override public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
|
||||
String originalIndent = writer.getIndent();
|
||||
writer.setIndent(indent);
|
||||
try {
|
||||
|
@ -218,21 +252,29 @@ public abstract class JsonAdapter<T> {
|
|||
writer.setIndent(originalIndent);
|
||||
}
|
||||
}
|
||||
@Override boolean isLenient() {
|
||||
return delegate.isLenient();
|
||||
}
|
||||
@Override public String toString() {
|
||||
return delegate + ".indent(\"" + indent + "\")";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
boolean isLenient() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public interface Factory {
|
||||
/**
|
||||
* Attempts to create an adapter for {@code type} annotated with {@code annotations}. This
|
||||
* returns the adapter if one was created, or null if this factory isn't capable of creating
|
||||
* such an adapter.
|
||||
*
|
||||
* <p>Implementations may use to {@link Moshi#adapter} to compose adapters of other types, or
|
||||
* <p>Implementations may use {@link Moshi#adapter} to compose adapters of other types, or
|
||||
* {@link Moshi#nextAdapter} to delegate to the underlying adapter of the same type.
|
||||
*/
|
||||
JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi);
|
||||
@CheckReturnValue
|
||||
@Nullable JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi);
|
||||
}
|
||||
}
|
||||
|
|
81
moshi/src/main/java/com/squareup/moshi/JsonClass.java
Normal file
81
moshi/src/main/java/com/squareup/moshi/JsonClass.java
Normal file
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
/**
|
||||
* Customizes how a type is encoded as JSON.
|
||||
*/
|
||||
@Retention(RUNTIME)
|
||||
@Documented
|
||||
public @interface JsonClass {
|
||||
/**
|
||||
* True to trigger the annotation processor to generate an adapter for this type.
|
||||
*
|
||||
* <p>There are currently some restrictions on which types that can be used with generated
|
||||
* adapters:
|
||||
* <ul>
|
||||
* <li>
|
||||
* The class must be implemented in Kotlin (unless using a custom generator, see
|
||||
* {@link #generator()}).
|
||||
* </li>
|
||||
* <li>The class may not be an abstract class, an inner class, or a local class.</li>
|
||||
* <li>All superclasses must be implemented in Kotlin.</li>
|
||||
* <li>All properties must be public, protected, or internal.</li>
|
||||
* <li>All properties must be either non-transient or have a default value.</li>
|
||||
* </ul>
|
||||
*/
|
||||
boolean generateAdapter();
|
||||
|
||||
/**
|
||||
* An optional custom generator tag used to indicate which generator should be used. If empty,
|
||||
* Moshi's annotation processor will generate an adapter for the annotated type. If not empty,
|
||||
* Moshi's processor will skip it and defer to a custom generator. This can be used to allow
|
||||
* other custom code generation tools to run and still allow Moshi to read their generated
|
||||
* JsonAdapter outputs.
|
||||
*
|
||||
* <p>Requirements for generated adapter class signatures:
|
||||
* <ul>
|
||||
* <li>
|
||||
* The generated adapter must subclass {@link JsonAdapter} and be parameterized by this type.
|
||||
* </li>
|
||||
* <li>
|
||||
* {@link Types#generatedJsonAdapterName} should be used for the fully qualified class name in
|
||||
* order for Moshi to correctly resolve and load the generated JsonAdapter.
|
||||
* </li>
|
||||
* <li>The first parameter must be a {@link Moshi} instance.</li>
|
||||
* <li>
|
||||
* If generic, a second {@link Type[]} parameter should be declared to accept type arguments.
|
||||
* </li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Example for a class "CustomType":<pre>{@code
|
||||
* class CustomTypeJsonAdapter(moshi: Moshi, types: Array<Type>) : JsonAdapter<CustomType>() {
|
||||
* // ...
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* <p>To help ensure your own generator meets requirements above, you can use Moshi’s built-in
|
||||
* generator to create the API signature to get started, then make your own generator match that
|
||||
* expected signature.
|
||||
*/
|
||||
String generator() default "";
|
||||
}
|
|
@ -15,6 +15,8 @@
|
|||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Thrown when the data in a JSON document doesn't match the data expected by the caller. For
|
||||
* example, suppose the application expects a boolean but the JSON document contains a string. When
|
||||
|
@ -31,15 +33,15 @@ public final class JsonDataException extends RuntimeException {
|
|||
public JsonDataException() {
|
||||
}
|
||||
|
||||
public JsonDataException(String message) {
|
||||
public JsonDataException(@Nullable String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public JsonDataException(Throwable cause) {
|
||||
public JsonDataException(@Nullable Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public JsonDataException(String message, Throwable cause) {
|
||||
public JsonDataException(@Nullable String message, @Nullable Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,10 +16,11 @@
|
|||
package com.squareup.moshi;
|
||||
|
||||
import java.io.IOException;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** Thrown when the data being parsed is not encoded as valid JSON. */
|
||||
public final class JsonEncodingException extends IOException {
|
||||
public JsonEncodingException(String message) {
|
||||
public JsonEncodingException(@Nullable String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,8 +18,11 @@ package com.squareup.moshi;
|
|||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
import javax.annotation.Nullable;
|
||||
import okio.Buffer;
|
||||
import okio.BufferedSource;
|
||||
import okio.ByteString;
|
||||
|
@ -118,7 +121,7 @@ import okio.ByteString;
|
|||
* id = reader.nextLong();
|
||||
* } else if (name.equals("text")) {
|
||||
* text = reader.nextString();
|
||||
* } else if (name.equals("geo") && reader.peek() != JsonToken.NULL) {
|
||||
* } else if (name.equals("geo") && reader.peek() != Token.NULL) {
|
||||
* geo = readDoublesArray(reader);
|
||||
* } else if (name.equals("user")) {
|
||||
* user = readUser(reader);
|
||||
|
@ -174,32 +177,50 @@ import okio.ByteString;
|
|||
* of this class are not thread safe.
|
||||
*/
|
||||
public abstract class JsonReader implements Closeable {
|
||||
// The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack permits
|
||||
// up to 32 levels of nesting including the top-level document. Deeper nesting is prone to trigger
|
||||
// StackOverflowErrors.
|
||||
int stackSize = 0;
|
||||
final int[] scopes = new int[32];
|
||||
final String[] pathNames = new String[32];
|
||||
final int[] pathIndices = new int[32];
|
||||
// The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack will
|
||||
// grow itself up to 256 levels of nesting including the top-level document. Deeper nesting is
|
||||
// prone to trigger StackOverflowErrors.
|
||||
int stackSize;
|
||||
int[] scopes;
|
||||
String[] pathNames;
|
||||
int[] pathIndices;
|
||||
|
||||
/** True to accept non-spec compliant JSON */
|
||||
/** True to accept non-spec compliant JSON. */
|
||||
boolean lenient;
|
||||
|
||||
/** True to throw a {@link JsonDataException} on any attempt to call {@link #skipValue()}. */
|
||||
boolean failOnUnknown;
|
||||
|
||||
/** Returns a new instance that reads UTF-8 encoded JSON from {@code source}. */
|
||||
public static JsonReader of(BufferedSource source) {
|
||||
@CheckReturnValue public static JsonReader of(BufferedSource source) {
|
||||
return new JsonUtf8Reader(source);
|
||||
}
|
||||
|
||||
// Package-private to control subclasses.
|
||||
JsonReader() {
|
||||
// Package-private to control subclasses.
|
||||
scopes = new int[32];
|
||||
pathNames = new String[32];
|
||||
pathIndices = new int[32];
|
||||
}
|
||||
|
||||
// Package-private to control subclasses.
|
||||
JsonReader(JsonReader copyFrom) {
|
||||
this.stackSize = copyFrom.stackSize;
|
||||
this.scopes = copyFrom.scopes.clone();
|
||||
this.pathNames = copyFrom.pathNames.clone();
|
||||
this.pathIndices = copyFrom.pathIndices.clone();
|
||||
this.lenient = copyFrom.lenient;
|
||||
this.failOnUnknown = copyFrom.failOnUnknown;
|
||||
}
|
||||
|
||||
final void pushScope(int newTop) {
|
||||
if (stackSize == scopes.length) {
|
||||
throw new JsonDataException("Nesting too deep at " + getPath());
|
||||
if (stackSize == 256) {
|
||||
throw new JsonDataException("Nesting too deep at " + getPath());
|
||||
}
|
||||
scopes = Arrays.copyOf(scopes, scopes.length * 2);
|
||||
pathNames = Arrays.copyOf(pathNames, pathNames.length * 2);
|
||||
pathIndices = Arrays.copyOf(pathIndices, pathIndices.length * 2);
|
||||
}
|
||||
scopes[stackSize++] = newTop;
|
||||
}
|
||||
|
@ -212,7 +233,7 @@ public abstract class JsonReader implements Closeable {
|
|||
throw new JsonEncodingException(message + " at path " + getPath());
|
||||
}
|
||||
|
||||
final JsonDataException typeMismatch(Object value, Object expected) {
|
||||
final JsonDataException typeMismatch(@Nullable Object value, Object expected) {
|
||||
if (value == null) {
|
||||
return new JsonDataException(
|
||||
"Expected " + expected + " but was null at path " + getPath());
|
||||
|
@ -254,7 +275,7 @@ public abstract class JsonReader implements Closeable {
|
|||
/**
|
||||
* Returns true if this parser is liberal in what it accepts.
|
||||
*/
|
||||
public final boolean isLenient() {
|
||||
@CheckReturnValue public final boolean isLenient() {
|
||||
return lenient;
|
||||
}
|
||||
|
||||
|
@ -271,9 +292,9 @@ public abstract class JsonReader implements Closeable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns true if this parser forbids skipping values.
|
||||
* Returns true if this parser forbids skipping names and values.
|
||||
*/
|
||||
public final boolean failOnUnknown() {
|
||||
@CheckReturnValue public final boolean failOnUnknown() {
|
||||
return failOnUnknown;
|
||||
}
|
||||
|
||||
|
@ -304,25 +325,34 @@ public abstract class JsonReader implements Closeable {
|
|||
/**
|
||||
* Returns true if the current array or object has another element.
|
||||
*/
|
||||
public abstract boolean hasNext() throws IOException;
|
||||
@CheckReturnValue public abstract boolean hasNext() throws IOException;
|
||||
|
||||
/**
|
||||
* Returns the type of the next token without consuming it.
|
||||
*/
|
||||
public abstract Token peek() throws IOException;
|
||||
@CheckReturnValue public abstract Token peek() throws IOException;
|
||||
|
||||
/**
|
||||
* Returns the next token, a {@linkplain Token#NAME property name}, and consumes it.
|
||||
*
|
||||
* @throws JsonDataException if the next token in the stream is not a property name.
|
||||
*/
|
||||
public abstract String nextName() throws IOException;
|
||||
@CheckReturnValue public abstract String nextName() throws IOException;
|
||||
|
||||
/**
|
||||
* If the next token is a {@linkplain Token#NAME property name} that's in {@code options}, this
|
||||
* consumes it and returns its index. Otherwise this returns -1 and no name is consumed.
|
||||
*/
|
||||
public abstract int selectName(Options options) throws IOException;
|
||||
@CheckReturnValue public abstract int selectName(Options options) throws IOException;
|
||||
|
||||
/**
|
||||
* Skips the next token, consuming it. This method is intended for use when the JSON token stream
|
||||
* contains unrecognized or unhandled names.
|
||||
*
|
||||
* <p>This throws a {@link JsonDataException} if this parser has been configured to {@linkplain
|
||||
* #failOnUnknown fail on unknown} names.
|
||||
*/
|
||||
public abstract void skipName() throws IOException;
|
||||
|
||||
/**
|
||||
* Returns the {@linkplain Token#STRING string} value of the next token, consuming it. If the next
|
||||
|
@ -336,7 +366,7 @@ public abstract class JsonReader implements Closeable {
|
|||
* If the next token is a {@linkplain Token#STRING string} that's in {@code options}, this
|
||||
* consumes it and returns its index. Otherwise this returns -1 and no string is consumed.
|
||||
*/
|
||||
public abstract int selectString(Options options) throws IOException;
|
||||
@CheckReturnValue public abstract int selectString(Options options) throws IOException;
|
||||
|
||||
/**
|
||||
* Returns the {@linkplain Token#BOOLEAN boolean} value of the next token, consuming it.
|
||||
|
@ -351,7 +381,7 @@ public abstract class JsonReader implements Closeable {
|
|||
*
|
||||
* @throws JsonDataException if the next token is not null or if this reader is closed.
|
||||
*/
|
||||
public abstract <T> T nextNull() throws IOException;
|
||||
public abstract @Nullable <T> T nextNull() throws IOException;
|
||||
|
||||
/**
|
||||
* Returns the {@linkplain Token#NUMBER double} value of the next token, consuming it. If the next
|
||||
|
@ -400,7 +430,7 @@ public abstract class JsonReader implements Closeable {
|
|||
* @throws JsonDataException if the next token is not a literal value, if a JSON object has a
|
||||
* duplicate key.
|
||||
*/
|
||||
public final Object readJsonValue() throws IOException {
|
||||
public final @Nullable Object readJsonValue() throws IOException {
|
||||
switch (peek()) {
|
||||
case BEGIN_ARRAY:
|
||||
List<Object> list = new ArrayList<>();
|
||||
|
@ -444,11 +474,37 @@ public abstract class JsonReader implements Closeable {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@code JsonReader} that can read data from this {@code JsonReader} without
|
||||
* consuming it. The returned reader becomes invalid once this one is next read or closed.
|
||||
*
|
||||
* <p>For example, we can use {@code peekJson()} to lookahead and read the same data multiple
|
||||
* times.
|
||||
*
|
||||
* <pre> {@code
|
||||
*
|
||||
* Buffer buffer = new Buffer();
|
||||
* buffer.writeUtf8("[123, 456, 789]")
|
||||
*
|
||||
* JsonReader jsonReader = JsonReader.of(buffer);
|
||||
* jsonReader.beginArray();
|
||||
* jsonReader.nextInt(); // Returns 123, reader contains 456, 789 and ].
|
||||
*
|
||||
* JsonReader peek = reader.peekJson();
|
||||
* peek.nextInt() // Returns 456.
|
||||
* peek.nextInt() // Returns 789.
|
||||
* peek.endArray()
|
||||
*
|
||||
* jsonReader.nextInt() // Returns 456, reader contains 789 and ].
|
||||
* }</pre>
|
||||
*/
|
||||
@CheckReturnValue public abstract JsonReader peekJson();
|
||||
|
||||
/**
|
||||
* Returns a <a href="http://goessner.net/articles/JsonPath/">JsonPath</a> to
|
||||
* the current location in the JSON value.
|
||||
*/
|
||||
public final String getPath() {
|
||||
@CheckReturnValue public final String getPath() {
|
||||
return JsonScope.getPath(stackSize, scopes, pathNames, pathIndices);
|
||||
}
|
||||
|
||||
|
@ -471,7 +527,7 @@ public abstract class JsonReader implements Closeable {
|
|||
this.doubleQuoteSuffix = doubleQuoteSuffix;
|
||||
}
|
||||
|
||||
public static Options of(String... strings) {
|
||||
@CheckReturnValue public static Options of(String... strings) {
|
||||
try {
|
||||
ByteString[] result = new ByteString[strings.length];
|
||||
Buffer buffer = new Buffer();
|
||||
|
|
|
@ -17,6 +17,8 @@ package com.squareup.moshi;
|
|||
|
||||
/** Lexical scoping elements within a JSON reader or writer. */
|
||||
final class JsonScope {
|
||||
private JsonScope() {
|
||||
}
|
||||
|
||||
/** An array with no elements requires no separators or newlines before it is closed. */
|
||||
static final int EMPTY_ARRAY = 1;
|
||||
|
|
|
@ -18,6 +18,7 @@ package com.squareup.moshi;
|
|||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import javax.annotation.Nullable;
|
||||
import okio.Buffer;
|
||||
import okio.BufferedSource;
|
||||
import okio.ByteString;
|
||||
|
@ -30,6 +31,7 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
private static final ByteString UNQUOTED_STRING_TERMINALS
|
||||
= ByteString.encodeUtf8("{}[]:, \n\t\r\f/\\;#=");
|
||||
private static final ByteString LINEFEED_OR_CARRIAGE_RETURN = ByteString.encodeUtf8("\n\r");
|
||||
private static final ByteString CLOSING_BLOCK_COMMENT = ByteString.encodeUtf8("*/");
|
||||
|
||||
private static final int PEEKED_NONE = 0;
|
||||
private static final int PEEKED_BEGIN_OBJECT = 1;
|
||||
|
@ -76,8 +78,7 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
private long peekedLong;
|
||||
|
||||
/**
|
||||
* The number of characters in a peeked number literal. Increment 'pos' by
|
||||
* this after reading a number.
|
||||
* The number of characters in a peeked number literal.
|
||||
*/
|
||||
private int peekedNumberLength;
|
||||
|
||||
|
@ -86,17 +87,38 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
* This is populated before a numeric value is parsed and used if that parsing
|
||||
* fails.
|
||||
*/
|
||||
private String peekedString;
|
||||
private @Nullable String peekedString;
|
||||
|
||||
JsonUtf8Reader(BufferedSource source) {
|
||||
if (source == null) {
|
||||
throw new NullPointerException("source == null");
|
||||
}
|
||||
this.source = source;
|
||||
this.buffer = source.buffer();
|
||||
this.buffer = source.getBuffer();
|
||||
pushScope(JsonScope.EMPTY_DOCUMENT);
|
||||
}
|
||||
|
||||
/** Copy-constructor makes a deep copy for peeking. */
|
||||
JsonUtf8Reader(JsonUtf8Reader copyFrom) {
|
||||
super(copyFrom);
|
||||
|
||||
BufferedSource sourcePeek = copyFrom.source.peek();
|
||||
this.source = sourcePeek;
|
||||
this.buffer = sourcePeek.getBuffer();
|
||||
this.peeked = copyFrom.peeked;
|
||||
this.peekedLong = copyFrom.peekedLong;
|
||||
this.peekedNumberLength = copyFrom.peekedNumberLength;
|
||||
this.peekedString = copyFrom.peekedString;
|
||||
|
||||
// Make sure our buffer has as many bytes as the source's buffer. This is necessary because
|
||||
// JsonUtf8Reader assumes any data it has peeked (like the peekedNumberLength) are buffered.
|
||||
try {
|
||||
sourcePeek.require(copyFrom.buffer.size());
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void beginArray() throws IOException {
|
||||
int p = peeked;
|
||||
if (p == PEEKED_NONE) {
|
||||
|
@ -162,7 +184,7 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
if (p == PEEKED_NONE) {
|
||||
p = doPeek();
|
||||
}
|
||||
return p != PEEKED_END_OBJECT && p != PEEKED_END_ARRAY;
|
||||
return p != PEEKED_END_OBJECT && p != PEEKED_END_ARRAY && p != PEEKED_EOF;
|
||||
}
|
||||
|
||||
@Override public Token peek() throws IOException {
|
||||
|
@ -464,7 +486,8 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
}
|
||||
|
||||
// We've read a complete number. Decide if it's a PEEKED_LONG or a PEEKED_NUMBER.
|
||||
if (last == NUMBER_CHAR_DIGIT && fitsInLong && (value != Long.MIN_VALUE || negative)) {
|
||||
if (last == NUMBER_CHAR_DIGIT && fitsInLong && (value != Long.MIN_VALUE || negative)
|
||||
&& (value != 0 || !negative)) {
|
||||
peekedLong = negative ? value : -value;
|
||||
buffer.skip(i);
|
||||
return peeked = PEEKED_LONG;
|
||||
|
@ -561,8 +584,29 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
return result;
|
||||
}
|
||||
|
||||
@Override public void skipName() throws IOException {
|
||||
if (failOnUnknown) {
|
||||
throw new JsonDataException("Cannot skip unexpected " + peek() + " at " + getPath());
|
||||
}
|
||||
int p = peeked;
|
||||
if (p == PEEKED_NONE) {
|
||||
p = doPeek();
|
||||
}
|
||||
if (p == PEEKED_UNQUOTED_NAME) {
|
||||
skipUnquotedValue();
|
||||
} else if (p == PEEKED_DOUBLE_QUOTED_NAME) {
|
||||
skipQuotedValue(DOUBLE_QUOTE_OR_SLASH);
|
||||
} else if (p == PEEKED_SINGLE_QUOTED_NAME) {
|
||||
skipQuotedValue(SINGLE_QUOTE_OR_SLASH);
|
||||
} else if (p != PEEKED_BUFFERED_NAME) {
|
||||
throw new JsonDataException("Expected a name but was " + peek() + " at path " + getPath());
|
||||
}
|
||||
peeked = PEEKED_NONE;
|
||||
pathNames[stackSize - 1] = "null";
|
||||
}
|
||||
|
||||
/**
|
||||
* If {@code name} is in {@code options} this consumes it and returns it's index.
|
||||
* If {@code name} is in {@code options} this consumes it and returns its index.
|
||||
* Otherwise this returns -1 and no name is consumed.
|
||||
*/
|
||||
private int findName(String name, Options options) {
|
||||
|
@ -637,7 +681,7 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
}
|
||||
|
||||
/**
|
||||
* If {@code string} is in {@code options} this consumes it and returns it's index.
|
||||
* If {@code string} is in {@code options} this consumes it and returns its index.
|
||||
* Otherwise this returns -1 and no string is consumed.
|
||||
*/
|
||||
private int findString(String string, Options options) {
|
||||
|
@ -669,7 +713,7 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
throw new JsonDataException("Expected a boolean but was " + peek() + " at path " + getPath());
|
||||
}
|
||||
|
||||
@Override public <T> T nextNull() throws IOException {
|
||||
@Override public @Nullable <T> T nextNull() throws IOException {
|
||||
int p = peeked;
|
||||
if (p == PEEKED_NONE) {
|
||||
p = doPeek();
|
||||
|
@ -749,7 +793,7 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
pathIndices[stackSize - 1]++;
|
||||
return result;
|
||||
} catch (NumberFormatException ignored) {
|
||||
// Fall back to parse as a double below.
|
||||
// Fall back to parse as a BigDecimal below.
|
||||
}
|
||||
} else if (p != PEEKED_BUFFERED) {
|
||||
throw new JsonDataException("Expected a long but was " + peek()
|
||||
|
@ -913,11 +957,19 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
pushScope(JsonScope.EMPTY_OBJECT);
|
||||
count++;
|
||||
} else if (p == PEEKED_END_ARRAY) {
|
||||
stackSize--;
|
||||
count--;
|
||||
if (count < 0) {
|
||||
throw new JsonDataException(
|
||||
"Expected a value but was " + peek() + " at path " + getPath());
|
||||
}
|
||||
stackSize--;
|
||||
} else if (p == PEEKED_END_OBJECT) {
|
||||
stackSize--;
|
||||
count--;
|
||||
if (count < 0) {
|
||||
throw new JsonDataException(
|
||||
"Expected a value but was " + peek() + " at path " + getPath());
|
||||
}
|
||||
stackSize--;
|
||||
} else if (p == PEEKED_UNQUOTED_NAME || p == PEEKED_UNQUOTED) {
|
||||
skipUnquotedValue();
|
||||
} else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_DOUBLE_QUOTED_NAME) {
|
||||
|
@ -926,6 +978,9 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
skipQuotedValue(SINGLE_QUOTE_OR_SLASH);
|
||||
} else if (p == PEEKED_NUMBER) {
|
||||
buffer.skip(peekedNumberLength);
|
||||
} else if (p == PEEKED_EOF) {
|
||||
throw new JsonDataException(
|
||||
"Expected a value but was " + peek() + " at path " + getPath());
|
||||
}
|
||||
peeked = PEEKED_NONE;
|
||||
} while (count != 0);
|
||||
|
@ -937,8 +992,7 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
/**
|
||||
* Returns the next character in the stream that is neither whitespace nor a
|
||||
* part of a comment. When this returns, the returned character is always at
|
||||
* {@code buffer[pos-1]}; this means the caller can always push back the
|
||||
* returned character by decrementing {@code pos}.
|
||||
* {@code buffer.getByte(0)}.
|
||||
*/
|
||||
private int nextNonWhitespace(boolean throwOnEof) throws IOException {
|
||||
/*
|
||||
|
@ -969,11 +1023,9 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
// skip a /* c-style comment */
|
||||
buffer.readByte(); // '/'
|
||||
buffer.readByte(); // '*'
|
||||
if (!skipTo("*/")) {
|
||||
if (!skipToEndOfBlockComment()) {
|
||||
throw syntaxError("Unterminated comment");
|
||||
}
|
||||
buffer.readByte(); // '*'
|
||||
buffer.readByte(); // '/'
|
||||
p = 0;
|
||||
continue;
|
||||
|
||||
|
@ -1022,20 +1074,17 @@ final class JsonUtf8Reader extends JsonReader {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param toFind a string to search for. Must not contain a newline.
|
||||
* Skips through the next closing block comment.
|
||||
*/
|
||||
private boolean skipTo(String toFind) throws IOException {
|
||||
outer:
|
||||
for (; source.request(toFind.length()); ) {
|
||||
for (int c = 0; c < toFind.length(); c++) {
|
||||
if (buffer.getByte(c) != toFind.charAt(c)) {
|
||||
buffer.readByte();
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
private boolean skipToEndOfBlockComment() throws IOException {
|
||||
long index = source.indexOf(CLOSING_BLOCK_COMMENT);
|
||||
boolean found = index != -1;
|
||||
buffer.skip(found ? index + CLOSING_BLOCK_COMMENT.size() : buffer.size());
|
||||
return found;
|
||||
}
|
||||
|
||||
@Override public JsonReader peekJson() {
|
||||
return new JsonUtf8Reader(this);
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
|
|
|
@ -16,7 +16,9 @@
|
|||
package com.squareup.moshi;
|
||||
|
||||
import java.io.IOException;
|
||||
import javax.annotation.Nullable;
|
||||
import okio.BufferedSink;
|
||||
import okio.BufferedSource;
|
||||
import okio.Sink;
|
||||
|
||||
import static com.squareup.moshi.JsonScope.DANGLING_NAME;
|
||||
|
@ -76,8 +78,12 @@ final class JsonUtf8Writer extends JsonWriter {
|
|||
}
|
||||
|
||||
@Override public JsonWriter beginArray() throws IOException {
|
||||
if (promoteValueToName) {
|
||||
throw new IllegalStateException(
|
||||
"Array cannot be used as a map key in JSON at path " + getPath());
|
||||
}
|
||||
writeDeferredName();
|
||||
return open(EMPTY_ARRAY, "[");
|
||||
return open(EMPTY_ARRAY, NONEMPTY_ARRAY, "[");
|
||||
}
|
||||
|
||||
@Override public JsonWriter endArray() throws IOException {
|
||||
|
@ -85,8 +91,12 @@ final class JsonUtf8Writer extends JsonWriter {
|
|||
}
|
||||
|
||||
@Override public JsonWriter beginObject() throws IOException {
|
||||
if (promoteValueToName) {
|
||||
throw new IllegalStateException(
|
||||
"Object cannot be used as a map key in JSON at path " + getPath());
|
||||
}
|
||||
writeDeferredName();
|
||||
return open(EMPTY_OBJECT, "{");
|
||||
return open(EMPTY_OBJECT, NONEMPTY_OBJECT, "{");
|
||||
}
|
||||
|
||||
@Override public JsonWriter endObject() throws IOException {
|
||||
|
@ -98,8 +108,15 @@ final class JsonUtf8Writer extends JsonWriter {
|
|||
* Enters a new scope by appending any necessary whitespace and the given
|
||||
* bracket.
|
||||
*/
|
||||
private JsonWriter open(int empty, String openBracket) throws IOException {
|
||||
private JsonWriter open(int empty, int nonempty, String openBracket) throws IOException {
|
||||
if (stackSize == flattenStackSize
|
||||
&& (scopes[stackSize - 1] == empty || scopes[stackSize - 1] == nonempty)) {
|
||||
// Cancel this open. Invert the flatten stack size until this is closed.
|
||||
flattenStackSize = ~flattenStackSize;
|
||||
return this;
|
||||
}
|
||||
beforeValue();
|
||||
checkStack();
|
||||
pushScope(empty);
|
||||
pathIndices[stackSize - 1] = 0;
|
||||
sink.writeUtf8(openBracket);
|
||||
|
@ -118,6 +135,11 @@ final class JsonUtf8Writer extends JsonWriter {
|
|||
if (deferredName != null) {
|
||||
throw new IllegalStateException("Dangling name: " + deferredName);
|
||||
}
|
||||
if (stackSize == ~flattenStackSize) {
|
||||
// Cancel this close. Restore the flattenStackSize so we're ready to flatten again!
|
||||
flattenStackSize = ~flattenStackSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
stackSize--;
|
||||
pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected!
|
||||
|
@ -136,7 +158,8 @@ final class JsonUtf8Writer extends JsonWriter {
|
|||
if (stackSize == 0) {
|
||||
throw new IllegalStateException("JsonWriter is closed.");
|
||||
}
|
||||
if (deferredName != null) {
|
||||
int context = peekScope();
|
||||
if ((context != EMPTY_OBJECT && context != NONEMPTY_OBJECT) || deferredName != null) {
|
||||
throw new IllegalStateException("Nesting problem.");
|
||||
}
|
||||
deferredName = name;
|
||||
|
@ -168,6 +191,10 @@ final class JsonUtf8Writer extends JsonWriter {
|
|||
}
|
||||
|
||||
@Override public JsonWriter nullValue() throws IOException {
|
||||
if (promoteValueToName) {
|
||||
throw new IllegalStateException(
|
||||
"null cannot be used as a map key in JSON at path " + getPath());
|
||||
}
|
||||
if (deferredName != null) {
|
||||
if (serializeNulls) {
|
||||
writeDeferredName();
|
||||
|
@ -183,6 +210,10 @@ final class JsonUtf8Writer extends JsonWriter {
|
|||
}
|
||||
|
||||
@Override public JsonWriter value(boolean value) throws IOException {
|
||||
if (promoteValueToName) {
|
||||
throw new IllegalStateException(
|
||||
"Boolean cannot be used as a map key in JSON at path " + getPath());
|
||||
}
|
||||
writeDeferredName();
|
||||
beforeValue();
|
||||
sink.writeUtf8(value ? "true" : "false");
|
||||
|
@ -222,7 +253,7 @@ final class JsonUtf8Writer extends JsonWriter {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override public JsonWriter value(Number value) throws IOException {
|
||||
@Override public JsonWriter value(@Nullable Number value) throws IOException {
|
||||
if (value == null) {
|
||||
return nullValue();
|
||||
}
|
||||
|
@ -242,6 +273,18 @@ final class JsonUtf8Writer extends JsonWriter {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override public JsonWriter value(BufferedSource source) throws IOException {
|
||||
if (promoteValueToName) {
|
||||
throw new IllegalStateException(
|
||||
"BufferedSource cannot be used as a map key in JSON at path " + getPath());
|
||||
}
|
||||
writeDeferredName();
|
||||
beforeValue();
|
||||
sink.writeAll(source);
|
||||
pathIndices[stackSize - 1]++;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures all buffered data is written to the underlying {@link Sink}
|
||||
* and flushes that writer.
|
||||
|
|
|
@ -20,8 +20,10 @@ import java.math.BigDecimal;
|
|||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static com.squareup.moshi.JsonScope.CLOSED;
|
||||
|
||||
/**
|
||||
* This class reads a JSON document by traversing a Java object comprising maps, lists, and JSON
|
||||
|
@ -31,11 +33,11 @@ import java.util.Map;
|
|||
* <ul>
|
||||
* <li>The next element to act upon is on the top of the stack.
|
||||
* <li>When the top of the stack is a {@link List}, calling {@link #beginArray()} replaces the
|
||||
* list with a {@link ListIterator}. The first element of the iterator is pushed on top of the
|
||||
* list with a {@link JsonIterator}. The first element of the iterator is pushed on top of the
|
||||
* iterator.
|
||||
* <li>Similarly, when the top of the stack is a {@link Map}, calling {@link #beginObject()}
|
||||
* replaces the map with an {@link Iterator} of its entries. The first element of the iterator
|
||||
* is pushed on top of the iterator.
|
||||
* replaces the map with an {@link JsonIterator} of its entries. The first element of the
|
||||
* iterator is pushed on top of the iterator.
|
||||
* <li>When the top of the stack is a {@link Map.Entry}, calling {@link #nextName()} returns the
|
||||
* entry's key and replaces the entry with its value on the stack.
|
||||
* <li>When an element is consumed it is popped. If the new top of the stack has a non-exhausted
|
||||
|
@ -48,17 +50,31 @@ final class JsonValueReader extends JsonReader {
|
|||
/** Sentinel object pushed on {@link #stack} when the reader is closed. */
|
||||
private static final Object JSON_READER_CLOSED = new Object();
|
||||
|
||||
private final Object[] stack = new Object[32];
|
||||
private Object[] stack;
|
||||
|
||||
public JsonValueReader(Object root) {
|
||||
JsonValueReader(Object root) {
|
||||
scopes[stackSize] = JsonScope.NONEMPTY_DOCUMENT;
|
||||
stack = new Object[32];
|
||||
stack[stackSize++] = root;
|
||||
}
|
||||
|
||||
/** Copy-constructor makes a deep copy for peeking. */
|
||||
JsonValueReader(JsonValueReader copyFrom) {
|
||||
super(copyFrom);
|
||||
|
||||
stack = copyFrom.stack.clone();
|
||||
for (int i = 0; i < stackSize; i++) {
|
||||
if (stack[i] instanceof JsonIterator) {
|
||||
stack[i] = ((JsonIterator) stack[i]).clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void beginArray() throws IOException {
|
||||
List<?> peeked = require(List.class, Token.BEGIN_ARRAY);
|
||||
|
||||
ListIterator<?> iterator = peeked.listIterator();
|
||||
JsonIterator iterator = new JsonIterator(
|
||||
Token.END_ARRAY, peeked.toArray(new Object[peeked.size()]), 0);
|
||||
stack[stackSize - 1] = iterator;
|
||||
scopes[stackSize - 1] = JsonScope.EMPTY_ARRAY;
|
||||
pathIndices[stackSize - 1] = 0;
|
||||
|
@ -70,8 +86,8 @@ final class JsonValueReader extends JsonReader {
|
|||
}
|
||||
|
||||
@Override public void endArray() throws IOException {
|
||||
ListIterator<?> peeked = require(ListIterator.class, Token.END_ARRAY);
|
||||
if (peeked.hasNext()) {
|
||||
JsonIterator peeked = require(JsonIterator.class, Token.END_ARRAY);
|
||||
if (peeked.endToken != Token.END_ARRAY || peeked.hasNext()) {
|
||||
throw typeMismatch(peeked, Token.END_ARRAY);
|
||||
}
|
||||
remove();
|
||||
|
@ -80,7 +96,8 @@ final class JsonValueReader extends JsonReader {
|
|||
@Override public void beginObject() throws IOException {
|
||||
Map<?, ?> peeked = require(Map.class, Token.BEGIN_OBJECT);
|
||||
|
||||
Iterator<?> iterator = peeked.entrySet().iterator();
|
||||
JsonIterator iterator = new JsonIterator(
|
||||
Token.END_OBJECT, peeked.entrySet().toArray(new Object[peeked.size()]), 0);
|
||||
stack[stackSize - 1] = iterator;
|
||||
scopes[stackSize - 1] = JsonScope.EMPTY_OBJECT;
|
||||
|
||||
|
@ -91,8 +108,8 @@ final class JsonValueReader extends JsonReader {
|
|||
}
|
||||
|
||||
@Override public void endObject() throws IOException {
|
||||
Iterator<?> peeked = require(Iterator.class, Token.END_OBJECT);
|
||||
if (peeked instanceof ListIterator || peeked.hasNext()) {
|
||||
JsonIterator peeked = require(JsonIterator.class, Token.END_OBJECT);
|
||||
if (peeked.endToken != Token.END_OBJECT || peeked.hasNext()) {
|
||||
throw typeMismatch(peeked, Token.END_OBJECT);
|
||||
}
|
||||
pathNames[stackSize - 1] = null;
|
||||
|
@ -100,8 +117,7 @@ final class JsonValueReader extends JsonReader {
|
|||
}
|
||||
|
||||
@Override public boolean hasNext() throws IOException {
|
||||
// TODO(jwilson): this is consistent with BufferedSourceJsonReader but it doesn't make sense.
|
||||
if (stackSize == 0) return true;
|
||||
if (stackSize == 0) return false;
|
||||
|
||||
Object peeked = stack[stackSize - 1];
|
||||
return !(peeked instanceof Iterator) || ((Iterator) peeked).hasNext();
|
||||
|
@ -112,8 +128,7 @@ final class JsonValueReader extends JsonReader {
|
|||
|
||||
// If the top of the stack is an iterator, take its first element and push it on the stack.
|
||||
Object peeked = stack[stackSize - 1];
|
||||
if (peeked instanceof ListIterator) return Token.END_ARRAY;
|
||||
if (peeked instanceof Iterator) return Token.END_OBJECT;
|
||||
if (peeked instanceof JsonIterator) return ((JsonIterator) peeked).endToken;
|
||||
if (peeked instanceof List) return Token.BEGIN_ARRAY;
|
||||
if (peeked instanceof Map) return Token.BEGIN_OBJECT;
|
||||
if (peeked instanceof Map.Entry) return Token.NAME;
|
||||
|
@ -150,16 +165,47 @@ final class JsonValueReader extends JsonReader {
|
|||
return -1;
|
||||
}
|
||||
|
||||
@Override public void skipName() throws IOException {
|
||||
if (failOnUnknown) {
|
||||
throw new JsonDataException("Cannot skip unexpected " + peek() + " at " + getPath());
|
||||
}
|
||||
|
||||
Map.Entry<?, ?> peeked = require(Map.Entry.class, Token.NAME);
|
||||
|
||||
// Swap the Map.Entry for its value on the stack.
|
||||
stack[stackSize - 1] = peeked.getValue();
|
||||
pathNames[stackSize - 2] = "null";
|
||||
}
|
||||
|
||||
@Override public String nextString() throws IOException {
|
||||
String peeked = require(String.class, Token.STRING);
|
||||
remove();
|
||||
return peeked;
|
||||
Object peeked = (stackSize != 0 ? stack[stackSize - 1] : null);
|
||||
if (peeked instanceof String) {
|
||||
remove();
|
||||
return (String) peeked;
|
||||
}
|
||||
if (peeked instanceof Number) {
|
||||
remove();
|
||||
return peeked.toString();
|
||||
}
|
||||
if (peeked == JSON_READER_CLOSED) {
|
||||
throw new IllegalStateException("JsonReader is closed");
|
||||
}
|
||||
throw typeMismatch(peeked, Token.STRING);
|
||||
}
|
||||
|
||||
@Override public int selectString(Options options) throws IOException {
|
||||
String peeked = require(String.class, Token.STRING);
|
||||
Object peeked = (stackSize != 0 ? stack[stackSize - 1] : null);
|
||||
|
||||
if (!(peeked instanceof String)) {
|
||||
if (peeked == JSON_READER_CLOSED) {
|
||||
throw new IllegalStateException("JsonReader is closed");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
String peekedString = (String) peeked;
|
||||
|
||||
for (int i = 0, length = options.strings.length; i < length; i++) {
|
||||
if (options.strings[i].equals(peeked)) {
|
||||
if (options.strings[i].equals(peekedString)) {
|
||||
remove();
|
||||
return i;
|
||||
}
|
||||
|
@ -173,7 +219,7 @@ final class JsonValueReader extends JsonReader {
|
|||
return peeked;
|
||||
}
|
||||
|
||||
@Override public <T> T nextNull() throws IOException {
|
||||
@Override public @Nullable <T> T nextNull() throws IOException {
|
||||
require(Void.class, Token.NULL);
|
||||
remove();
|
||||
return null;
|
||||
|
@ -262,6 +308,9 @@ final class JsonValueReader extends JsonReader {
|
|||
|
||||
Object skipped = stackSize != 0 ? stack[stackSize - 1] : null;
|
||||
|
||||
if (skipped instanceof JsonIterator) {
|
||||
throw new JsonDataException("Expected a value but was " + peek() + " at path " + getPath());
|
||||
}
|
||||
if (skipped instanceof Map.Entry) {
|
||||
// We're skipping a name. Promote the map entry's value.
|
||||
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) stack[stackSize - 1];
|
||||
|
@ -269,9 +318,15 @@ final class JsonValueReader extends JsonReader {
|
|||
} else if (stackSize > 0) {
|
||||
// We're skipping a value.
|
||||
remove();
|
||||
} else {
|
||||
throw new JsonDataException("Expected a value but was " + peek() + " at path " + getPath());
|
||||
}
|
||||
}
|
||||
|
||||
@Override public JsonReader peekJson() {
|
||||
return new JsonValueReader(this);
|
||||
}
|
||||
|
||||
@Override void promoteNameToValue() throws IOException {
|
||||
if (hasNext()) {
|
||||
String name = nextName();
|
||||
|
@ -282,13 +337,19 @@ final class JsonValueReader extends JsonReader {
|
|||
@Override public void close() throws IOException {
|
||||
Arrays.fill(stack, 0, stackSize, null);
|
||||
stack[0] = JSON_READER_CLOSED;
|
||||
scopes[0] = JsonScope.CLOSED;
|
||||
scopes[0] = CLOSED;
|
||||
stackSize = 1;
|
||||
}
|
||||
|
||||
private void push(Object newTop) {
|
||||
if (stackSize == stack.length) {
|
||||
throw new JsonDataException("Nesting too deep at " + getPath());
|
||||
if (stackSize == 256) {
|
||||
throw new JsonDataException("Nesting too deep at " + getPath());
|
||||
}
|
||||
scopes = Arrays.copyOf(scopes, scopes.length * 2);
|
||||
pathNames = Arrays.copyOf(pathNames, pathNames.length * 2);
|
||||
pathIndices = Arrays.copyOf(pathIndices, pathIndices.length * 2);
|
||||
stack = Arrays.copyOf(stack, stack.length * 2);
|
||||
}
|
||||
stack[stackSize++] = newTop;
|
||||
}
|
||||
|
@ -297,7 +358,7 @@ final class JsonValueReader extends JsonReader {
|
|||
* Returns the top of the stack which is required to be a {@code type}. Throws if this reader is
|
||||
* closed, or if the type isn't what was expected.
|
||||
*/
|
||||
private <T> T require(Class<T> type, Token expected) throws IOException {
|
||||
private @Nullable <T> T require(Class<T> type, Token expected) throws IOException {
|
||||
Object peeked = (stackSize != 0 ? stack[stackSize - 1] : null);
|
||||
|
||||
if (type.isInstance(peeked)) {
|
||||
|
@ -337,4 +398,33 @@ final class JsonValueReader extends JsonReader {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
static final class JsonIterator implements Iterator<Object>, Cloneable {
|
||||
final Token endToken;
|
||||
final Object[] array;
|
||||
int next;
|
||||
|
||||
JsonIterator(Token endToken, Object[] array, int next) {
|
||||
this.endToken = endToken;
|
||||
this.array = array;
|
||||
this.next = next;
|
||||
}
|
||||
|
||||
@Override public boolean hasNext() {
|
||||
return next < array.length;
|
||||
}
|
||||
|
||||
@Override public Object next() {
|
||||
return array[next++];
|
||||
}
|
||||
|
||||
@Override public void remove() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override protected JsonIterator clone() {
|
||||
// No need to copy the array; it's read-only.
|
||||
return new JsonIterator(endToken, array, next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@ import java.math.BigDecimal;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
import okio.BufferedSource;
|
||||
|
||||
import static com.squareup.moshi.JsonScope.EMPTY_ARRAY;
|
||||
import static com.squareup.moshi.JsonScope.EMPTY_DOCUMENT;
|
||||
|
@ -30,8 +32,8 @@ import static java.lang.Double.POSITIVE_INFINITY;
|
|||
|
||||
/** Writes JSON by building a Java object comprising maps, lists, and JSON primitives. */
|
||||
final class JsonValueWriter extends JsonWriter {
|
||||
private final Object[] stack = new Object[32];
|
||||
private String deferredName;
|
||||
Object[] stack = new Object[32];
|
||||
private @Nullable String deferredName;
|
||||
|
||||
JsonValueWriter() {
|
||||
pushScope(EMPTY_DOCUMENT);
|
||||
|
@ -46,9 +48,16 @@ final class JsonValueWriter extends JsonWriter {
|
|||
}
|
||||
|
||||
@Override public JsonWriter beginArray() throws IOException {
|
||||
if (stackSize == stack.length) {
|
||||
throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?");
|
||||
if (promoteValueToName) {
|
||||
throw new IllegalStateException(
|
||||
"Array cannot be used as a map key in JSON at path " + getPath());
|
||||
}
|
||||
if (stackSize == flattenStackSize && scopes[stackSize - 1] == EMPTY_ARRAY) {
|
||||
// Cancel this open. Invert the flatten stack size until this is closed.
|
||||
flattenStackSize = ~flattenStackSize;
|
||||
return this;
|
||||
}
|
||||
checkStack();
|
||||
List<Object> list = new ArrayList<>();
|
||||
add(list);
|
||||
stack[stackSize] = list;
|
||||
|
@ -61,6 +70,11 @@ final class JsonValueWriter extends JsonWriter {
|
|||
if (peekScope() != EMPTY_ARRAY) {
|
||||
throw new IllegalStateException("Nesting problem.");
|
||||
}
|
||||
if (stackSize == ~flattenStackSize) {
|
||||
// Cancel this close. Restore the flattenStackSize so we're ready to flatten again!
|
||||
flattenStackSize = ~flattenStackSize;
|
||||
return this;
|
||||
}
|
||||
stackSize--;
|
||||
stack[stackSize] = null;
|
||||
pathIndices[stackSize - 1]++;
|
||||
|
@ -68,9 +82,16 @@ final class JsonValueWriter extends JsonWriter {
|
|||
}
|
||||
|
||||
@Override public JsonWriter beginObject() throws IOException {
|
||||
if (stackSize == stack.length) {
|
||||
throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?");
|
||||
if (promoteValueToName) {
|
||||
throw new IllegalStateException(
|
||||
"Object cannot be used as a map key in JSON at path " + getPath());
|
||||
}
|
||||
if (stackSize == flattenStackSize && scopes[stackSize - 1] == EMPTY_OBJECT) {
|
||||
// Cancel this open. Invert the flatten stack size until this is closed.
|
||||
flattenStackSize = ~flattenStackSize;
|
||||
return this;
|
||||
}
|
||||
checkStack();
|
||||
Map<String, Object> map = new LinkedHashTreeMap<>();
|
||||
add(map);
|
||||
stack[stackSize] = map;
|
||||
|
@ -79,9 +100,17 @@ final class JsonValueWriter extends JsonWriter {
|
|||
}
|
||||
|
||||
@Override public JsonWriter endObject() throws IOException {
|
||||
if (peekScope() != EMPTY_OBJECT || deferredName != null) {
|
||||
if (peekScope() != EMPTY_OBJECT) {
|
||||
throw new IllegalStateException("Nesting problem.");
|
||||
}
|
||||
if (deferredName != null) {
|
||||
throw new IllegalStateException("Dangling name: " + deferredName);
|
||||
}
|
||||
if (stackSize == ~flattenStackSize) {
|
||||
// Cancel this close. Restore the flattenStackSize so we're ready to flatten again!
|
||||
flattenStackSize = ~flattenStackSize;
|
||||
return this;
|
||||
}
|
||||
promoteValueToName = false;
|
||||
stackSize--;
|
||||
stack[stackSize] = null;
|
||||
|
@ -106,7 +135,7 @@ final class JsonValueWriter extends JsonWriter {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override public JsonWriter value(String value) throws IOException {
|
||||
@Override public JsonWriter value(@Nullable String value) throws IOException {
|
||||
if (promoteValueToName) {
|
||||
return name(value);
|
||||
}
|
||||
|
@ -116,18 +145,30 @@ final class JsonValueWriter extends JsonWriter {
|
|||
}
|
||||
|
||||
@Override public JsonWriter nullValue() throws IOException {
|
||||
if (promoteValueToName) {
|
||||
throw new IllegalStateException(
|
||||
"null cannot be used as a map key in JSON at path " + getPath());
|
||||
}
|
||||
add(null);
|
||||
pathIndices[stackSize - 1]++;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override public JsonWriter value(boolean value) throws IOException {
|
||||
if (promoteValueToName) {
|
||||
throw new IllegalStateException(
|
||||
"Boolean cannot be used as a map key in JSON at path " + getPath());
|
||||
}
|
||||
add(value);
|
||||
pathIndices[stackSize - 1]++;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override public JsonWriter value(Boolean value) throws IOException {
|
||||
@Override public JsonWriter value(@Nullable Boolean value) throws IOException {
|
||||
if (promoteValueToName) {
|
||||
throw new IllegalStateException(
|
||||
"Boolean cannot be used as a map key in JSON at path " + getPath());
|
||||
}
|
||||
add(value);
|
||||
pathIndices[stackSize - 1]++;
|
||||
return this;
|
||||
|
@ -155,7 +196,7 @@ final class JsonValueWriter extends JsonWriter {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override public JsonWriter value(Number value) throws IOException {
|
||||
@Override public JsonWriter value(@Nullable Number value) throws IOException {
|
||||
// If it's trivially converted to a long, do that.
|
||||
if (value instanceof Byte
|
||||
|| value instanceof Short
|
||||
|
@ -169,6 +210,10 @@ final class JsonValueWriter extends JsonWriter {
|
|||
return value(value.doubleValue());
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
return nullValue();
|
||||
}
|
||||
|
||||
// Everything else gets converted to a BigDecimal.
|
||||
BigDecimal bigDecimalValue = value instanceof BigDecimal
|
||||
? ((BigDecimal) value)
|
||||
|
@ -181,6 +226,23 @@ final class JsonValueWriter extends JsonWriter {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override public JsonWriter value(BufferedSource source) throws IOException {
|
||||
if (promoteValueToName) {
|
||||
throw new IllegalStateException(
|
||||
"BufferedSource cannot be used as a map key in JSON at path " + getPath());
|
||||
}
|
||||
Object value = JsonReader.of(source).readJsonValue();
|
||||
boolean serializeNulls = this.serializeNulls;
|
||||
this.serializeNulls = true;
|
||||
try {
|
||||
add(value);
|
||||
} finally {
|
||||
this.serializeNulls = serializeNulls;
|
||||
}
|
||||
pathIndices[stackSize - 1]++;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override public void close() throws IOException {
|
||||
int size = stackSize;
|
||||
if (size > 1 || size == 1 && scopes[size - 1] != NONEMPTY_DOCUMENT) {
|
||||
|
@ -195,7 +257,7 @@ final class JsonValueWriter extends JsonWriter {
|
|||
}
|
||||
}
|
||||
|
||||
private JsonValueWriter add(Object newTop) {
|
||||
private JsonValueWriter add(@Nullable Object newTop) {
|
||||
int scope = peekScope();
|
||||
|
||||
if (stackSize == 1) {
|
||||
|
|
|
@ -18,9 +18,15 @@ package com.squareup.moshi;
|
|||
import java.io.Closeable;
|
||||
import java.io.Flushable;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
import javax.annotation.Nullable;
|
||||
import okio.BufferedSink;
|
||||
import okio.BufferedSource;
|
||||
|
||||
import static com.squareup.moshi.JsonScope.EMPTY_ARRAY;
|
||||
import static com.squareup.moshi.JsonScope.EMPTY_OBJECT;
|
||||
import static com.squareup.moshi.JsonScope.NONEMPTY_ARRAY;
|
||||
import static com.squareup.moshi.JsonScope.NONEMPTY_OBJECT;
|
||||
|
||||
/**
|
||||
|
@ -119,13 +125,13 @@ import static com.squareup.moshi.JsonScope.NONEMPTY_OBJECT;
|
|||
* malformed JSON string will fail with an {@link IllegalStateException}.
|
||||
*/
|
||||
public abstract class JsonWriter implements Closeable, Flushable {
|
||||
// The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack permits
|
||||
// up to 32 levels of nesting including the top-level document. Deeper nesting is prone to trigger
|
||||
// StackOverflowErrors.
|
||||
// The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack will
|
||||
// grow itself up to 256 levels of nesting including the top-level document. Deeper nesting is
|
||||
// prone to trigger StackOverflowErrors.
|
||||
int stackSize = 0;
|
||||
final int[] scopes = new int[32];
|
||||
final String[] pathNames = new String[32];
|
||||
final int[] pathIndices = new int[32];
|
||||
int[] scopes = new int[32];
|
||||
String[] pathNames = new String[32];
|
||||
int[] pathIndices = new int[32];
|
||||
|
||||
/**
|
||||
* A string containing a full set of spaces for a single level of indentation, or null for no
|
||||
|
@ -136,8 +142,28 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
|||
boolean serializeNulls;
|
||||
boolean promoteValueToName;
|
||||
|
||||
/**
|
||||
* Controls the deepest stack size that has begin/end pairs flattened:
|
||||
*
|
||||
* <ul>
|
||||
* <li>If -1, no begin/end pairs are being suppressed.
|
||||
* <li>If positive, this is the deepest stack size whose begin/end pairs are eligible to be
|
||||
* flattened.
|
||||
* <li>If negative, it is the bitwise inverse (~) of the deepest stack size whose begin/end
|
||||
* pairs have been flattened.
|
||||
* </ul>
|
||||
*
|
||||
* <p>We differentiate between what layer would be flattened (positive) from what layer is being
|
||||
* flattened (negative) so that we don't double-flatten.
|
||||
*
|
||||
* <p>To accommodate nested flattening we require callers to track the previous state when they
|
||||
* provide a new state. The previous state is returned from {@link #beginFlatten} and restored
|
||||
* with {@link #endFlatten}.
|
||||
*/
|
||||
int flattenStackSize = -1;
|
||||
|
||||
/** Returns a new instance that writes UTF-8 encoded JSON to {@code sink}. */
|
||||
public static JsonWriter of(BufferedSink sink) {
|
||||
@CheckReturnValue public static JsonWriter of(BufferedSink sink) {
|
||||
return new JsonUtf8Writer(sink);
|
||||
}
|
||||
|
||||
|
@ -153,10 +179,26 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
|||
return scopes[stackSize - 1];
|
||||
}
|
||||
|
||||
final void pushScope(int newTop) {
|
||||
if (stackSize == scopes.length) {
|
||||
/** Before pushing a value on the stack this confirms that the stack has capacity. */
|
||||
final boolean checkStack() {
|
||||
if (stackSize != scopes.length) return false;
|
||||
|
||||
if (stackSize == 256) {
|
||||
throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?");
|
||||
}
|
||||
|
||||
scopes = Arrays.copyOf(scopes, scopes.length * 2);
|
||||
pathNames = Arrays.copyOf(pathNames, pathNames.length * 2);
|
||||
pathIndices = Arrays.copyOf(pathIndices, pathIndices.length * 2);
|
||||
if (this instanceof JsonValueWriter) {
|
||||
((JsonValueWriter) this).stack =
|
||||
Arrays.copyOf(((JsonValueWriter) this).stack, ((JsonValueWriter) this).stack.length * 2);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
final void pushScope(int newTop) {
|
||||
scopes[stackSize++] = newTop;
|
||||
}
|
||||
|
||||
|
@ -181,7 +223,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
|||
* Returns a string containing only whitespace, used for each level of
|
||||
* indentation. If empty, the encoded document will be compact.
|
||||
*/
|
||||
public final String getIndent() {
|
||||
@CheckReturnValue public final String getIndent() {
|
||||
return indent != null ? indent : "";
|
||||
}
|
||||
|
||||
|
@ -204,7 +246,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
|||
/**
|
||||
* Returns true if this writer has relaxed syntax rules.
|
||||
*/
|
||||
public final boolean isLenient() {
|
||||
@CheckReturnValue public final boolean isLenient() {
|
||||
return lenient;
|
||||
}
|
||||
|
||||
|
@ -220,7 +262,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
|||
* Returns true if object members are serialized when their value is null.
|
||||
* This has no impact on array elements. The default is false.
|
||||
*/
|
||||
public final boolean getSerializeNulls() {
|
||||
@CheckReturnValue public final boolean getSerializeNulls() {
|
||||
return serializeNulls;
|
||||
}
|
||||
|
||||
|
@ -257,7 +299,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
|||
/**
|
||||
* Encodes the property name.
|
||||
*
|
||||
* @param name the name of the forthcoming value. May not be null.
|
||||
* @param name the name of the forthcoming value. Must not be null.
|
||||
* @return this writer.
|
||||
*/
|
||||
public abstract JsonWriter name(String name) throws IOException;
|
||||
|
@ -268,7 +310,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
|||
* @param value the literal string value, or null to encode a null literal.
|
||||
* @return this writer.
|
||||
*/
|
||||
public abstract JsonWriter value(String value) throws IOException;
|
||||
public abstract JsonWriter value(@Nullable String value) throws IOException;
|
||||
|
||||
/**
|
||||
* Encodes {@code null}.
|
||||
|
@ -289,7 +331,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
|||
*
|
||||
* @return this writer.
|
||||
*/
|
||||
public abstract JsonWriter value(Boolean value) throws IOException;
|
||||
public abstract JsonWriter value(@Nullable Boolean value) throws IOException;
|
||||
|
||||
/**
|
||||
* Encodes {@code value}.
|
||||
|
@ -314,7 +356,16 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
|||
* {@linkplain Double#isInfinite() infinities}.
|
||||
* @return this writer.
|
||||
*/
|
||||
public abstract JsonWriter value(Number value) throws IOException;
|
||||
public abstract JsonWriter value(@Nullable Number value) throws IOException;
|
||||
|
||||
/**
|
||||
* Writes {@code source} directly without encoding its contents.
|
||||
* Since no validation is performed, {@link #setSerializeNulls} and other writer configurations
|
||||
* are not respected.
|
||||
*
|
||||
* @return this writer.
|
||||
*/
|
||||
public abstract JsonWriter value(BufferedSource source) throws IOException;
|
||||
|
||||
/**
|
||||
* Changes the writer to treat the next value as a string name. This is useful for map adapters so
|
||||
|
@ -328,11 +379,93 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
|||
promoteValueToName = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels immediately-nested calls to {@link #beginArray()} or {@link #beginObject()} and their
|
||||
* matching calls to {@link #endArray} or {@link #endObject()}. Use this to compose JSON adapters
|
||||
* without nesting.
|
||||
*
|
||||
* <p>For example, the following creates JSON with nested arrays: {@code [1,[2,3,4],5]}.
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* JsonAdapter<List<Integer>> integersAdapter = ...
|
||||
*
|
||||
* public void writeNumbers(JsonWriter writer) {
|
||||
* writer.beginArray();
|
||||
* writer.value(1);
|
||||
* integersAdapter.toJson(writer, Arrays.asList(2, 3, 4));
|
||||
* writer.value(5);
|
||||
* writer.endArray();
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* <p>With flattening we can create JSON with a single array {@code [1,2,3,4,5]}:
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* JsonAdapter<List<Integer>> integersAdapter = ...
|
||||
*
|
||||
* public void writeNumbers(JsonWriter writer) {
|
||||
* writer.beginArray();
|
||||
* int token = writer.beginFlatten();
|
||||
* writer.value(1);
|
||||
* integersAdapter.toJson(writer, Arrays.asList(2, 3, 4));
|
||||
* writer.value(5);
|
||||
* writer.endFlatten(token);
|
||||
* writer.endArray();
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* <p>This method flattens arrays within arrays:
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* Emit: [1, [2, 3, 4], 5]
|
||||
* To produce: [1, 2, 3, 4, 5]
|
||||
* }</pre>
|
||||
*
|
||||
* It also flattens objects within objects. Do not call {@link #name} before writing a flattened
|
||||
* object.
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* Emit: {"a": 1, {"b": 2}, "c": 3}
|
||||
* To Produce: {"a": 1, "b": 2, "c": 3}
|
||||
* }</pre>
|
||||
*
|
||||
* Other combinations are permitted but do not perform flattening. For example, objects inside of
|
||||
* arrays are not flattened:
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* Emit: [1, {"b": 2}, 3, [4, 5], 6]
|
||||
* To Produce: [1, {"b": 2}, 3, 4, 5, 6]
|
||||
* }</pre>
|
||||
*
|
||||
* <p>This method returns an opaque token. Callers must match all calls to this method with a call
|
||||
* to {@link #endFlatten} with the matching token.
|
||||
*/
|
||||
@CheckReturnValue public final int beginFlatten() {
|
||||
int context = peekScope();
|
||||
if (context != NONEMPTY_OBJECT && context != EMPTY_OBJECT
|
||||
&& context != NONEMPTY_ARRAY && context != EMPTY_ARRAY) {
|
||||
throw new IllegalStateException("Nesting problem.");
|
||||
}
|
||||
int token = flattenStackSize;
|
||||
flattenStackSize = stackSize;
|
||||
return token;
|
||||
}
|
||||
|
||||
/** Ends nested call flattening created by {@link #beginFlatten}. */
|
||||
public final void endFlatten(int token) {
|
||||
flattenStackSize = token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a <a href="http://goessner.net/articles/JsonPath/">JsonPath</a> to
|
||||
* the current location in the JSON value.
|
||||
*/
|
||||
public final String getPath() {
|
||||
@CheckReturnValue public final String getPath() {
|
||||
return JsonScope.getPath(stackSize, scopes, pathNames, pathIndices);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ final class LinkedHashTreeMap<K, V> extends AbstractMap<K, V> implements Seriali
|
|||
* Create a natural order, empty tree map whose keys must be mutually
|
||||
* comparable and non-null.
|
||||
*/
|
||||
public LinkedHashTreeMap() {
|
||||
LinkedHashTreeMap() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
|
@ -66,8 +66,10 @@ final class LinkedHashTreeMap<K, V> extends AbstractMap<K, V> implements Seriali
|
|||
* @param comparator the comparator to order elements with, or {@code null} to
|
||||
* use the natural ordering.
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" }) // unsafe! if comparator is null, this assumes K is comparable
|
||||
public LinkedHashTreeMap(Comparator<? super K> comparator) {
|
||||
@SuppressWarnings({
|
||||
"unchecked", "rawtypes" // Unsafe! if comparator is null, this assumes K is comparable.
|
||||
})
|
||||
LinkedHashTreeMap(Comparator<? super K> comparator) {
|
||||
this.comparator = comparator != null
|
||||
? comparator
|
||||
: (Comparator) NATURAL_ORDER;
|
||||
|
@ -473,14 +475,14 @@ final class LinkedHashTreeMap<K, V> extends AbstractMap<K, V> implements Seriali
|
|||
V value;
|
||||
int height;
|
||||
|
||||
/** Create the header entry */
|
||||
/** Create the header entry. */
|
||||
Node() {
|
||||
key = null;
|
||||
hash = -1;
|
||||
next = prev = this;
|
||||
}
|
||||
|
||||
/** Create a regular entry */
|
||||
/** Create a regular entry. */
|
||||
Node(Node<K, V> parent, K key, int hash, Node<K, V> next, Node<K, V> prev) {
|
||||
this.parent = parent;
|
||||
this.key = key;
|
||||
|
@ -665,7 +667,7 @@ final class LinkedHashTreeMap<K, V> extends AbstractMap<K, V> implements Seriali
|
|||
* comparisons. Using this class to create a tree of size <i>S</i> is
|
||||
* {@code O(S)}.
|
||||
*/
|
||||
final static class AvlBuilder<K, V> {
|
||||
static final class AvlBuilder<K, V> {
|
||||
/** This stack is a singly linked list, linked by the 'parent' field. */
|
||||
private Node<K, V> stack;
|
||||
private int leavesToSkip;
|
||||
|
|
|
@ -20,6 +20,7 @@ import java.lang.annotation.Annotation;
|
|||
import java.lang.reflect.Type;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Converts maps with string keys to JSON objects.
|
||||
|
@ -28,7 +29,7 @@ import java.util.Set;
|
|||
*/
|
||||
final class MapJsonAdapter<K, V> extends JsonAdapter<Map<K, V>> {
|
||||
public static final Factory FACTORY = new Factory() {
|
||||
@Override public JsonAdapter<?> create(
|
||||
@Override public @Nullable JsonAdapter<?> create(
|
||||
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
if (!annotations.isEmpty()) return null;
|
||||
Class<?> rawType = Types.getRawType(type);
|
||||
|
@ -41,7 +42,7 @@ final class MapJsonAdapter<K, V> extends JsonAdapter<Map<K, V>> {
|
|||
private final JsonAdapter<K> keyAdapter;
|
||||
private final JsonAdapter<V> valueAdapter;
|
||||
|
||||
public MapJsonAdapter(Moshi moshi, Type keyType, Type valueType) {
|
||||
MapJsonAdapter(Moshi moshi, Type keyType, Type valueType) {
|
||||
this.keyAdapter = moshi.adapter(keyType);
|
||||
this.valueAdapter = moshi.adapter(valueType);
|
||||
}
|
||||
|
|
|
@ -15,16 +15,27 @@
|
|||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import com.squareup.moshi.internal.Util;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static com.squareup.moshi.internal.Util.canonicalize;
|
||||
import static com.squareup.moshi.internal.Util.removeSubtypeWildcard;
|
||||
import static com.squareup.moshi.internal.Util.typeAnnotatedWithAnnotations;
|
||||
|
||||
/**
|
||||
* Coordinates binding between JSON values and Java objects.
|
||||
|
@ -41,7 +52,7 @@ public final class Moshi {
|
|||
}
|
||||
|
||||
private final List<JsonAdapter.Factory> factories;
|
||||
private final ThreadLocal<List<DeferredAdapter<?>>> reentrantCalls = new ThreadLocal<>();
|
||||
private final ThreadLocal<LookupChain> lookupChainThreadLocal = new ThreadLocal<>();
|
||||
private final Map<Object, JsonAdapter<?>> adapterCache = new LinkedHashMap<>();
|
||||
|
||||
Moshi(Builder builder) {
|
||||
|
@ -53,22 +64,56 @@ public final class Moshi {
|
|||
}
|
||||
|
||||
/** Returns a JSON adapter for {@code type}, creating it if necessary. */
|
||||
public <T> JsonAdapter<T> adapter(Type type) {
|
||||
@CheckReturnValue public <T> JsonAdapter<T> adapter(Type type) {
|
||||
return adapter(type, Util.NO_ANNOTATIONS);
|
||||
}
|
||||
|
||||
public <T> JsonAdapter<T> adapter(Class<T> type) {
|
||||
@CheckReturnValue public <T> JsonAdapter<T> adapter(Class<T> type) {
|
||||
return adapter(type, Util.NO_ANNOTATIONS);
|
||||
}
|
||||
|
||||
@CheckReturnValue
|
||||
public <T> JsonAdapter<T> adapter(Type type, Class<? extends Annotation> annotationType) {
|
||||
if (annotationType == null) {
|
||||
throw new NullPointerException("annotationType == null");
|
||||
}
|
||||
return adapter(type,
|
||||
Collections.singleton(Types.createJsonQualifierImplementation(annotationType)));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters.
|
||||
@CheckReturnValue
|
||||
public <T> JsonAdapter<T> adapter(Type type, Class<? extends Annotation>... annotationTypes) {
|
||||
if (annotationTypes.length == 1) {
|
||||
return adapter(type, annotationTypes[0]);
|
||||
}
|
||||
Set<Annotation> annotations = new LinkedHashSet<>(annotationTypes.length);
|
||||
for (Class<? extends Annotation> annotationType : annotationTypes) {
|
||||
annotations.add(Types.createJsonQualifierImplementation(annotationType));
|
||||
}
|
||||
return adapter(type, Collections.unmodifiableSet(annotations));
|
||||
}
|
||||
|
||||
@CheckReturnValue
|
||||
public <T> JsonAdapter<T> adapter(Type type, Set<? extends Annotation> annotations) {
|
||||
type = Types.canonicalize(type);
|
||||
return adapter(type, annotations, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param fieldName An optional field name associated with this type. The field name is used as a
|
||||
* hint for better adapter lookup error messages for nested structures.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
@SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters.
|
||||
public <T> JsonAdapter<T> adapter(Type type, Set<? extends Annotation> annotations,
|
||||
@Nullable String fieldName) {
|
||||
if (type == null) {
|
||||
throw new NullPointerException("type == null");
|
||||
}
|
||||
if (annotations == null) {
|
||||
throw new NullPointerException("annotations == null");
|
||||
}
|
||||
|
||||
type = removeSubtypeWildcard(canonicalize(type));
|
||||
|
||||
// If there's an equivalent adapter in the cache, we're done!
|
||||
Object cacheKey = cacheKey(type, annotations);
|
||||
|
@ -77,48 +122,44 @@ public final class Moshi {
|
|||
if (result != null) return (JsonAdapter<T>) result;
|
||||
}
|
||||
|
||||
// Short-circuit if this is a reentrant call.
|
||||
List<DeferredAdapter<?>> deferredAdapters = reentrantCalls.get();
|
||||
if (deferredAdapters != null) {
|
||||
for (int i = 0, size = deferredAdapters.size(); i < size; i++) {
|
||||
DeferredAdapter<?> deferredAdapter = deferredAdapters.get(i);
|
||||
if (deferredAdapter.cacheKey.equals(cacheKey)) {
|
||||
return (JsonAdapter<T>) deferredAdapter;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
deferredAdapters = new ArrayList<>();
|
||||
reentrantCalls.set(deferredAdapters);
|
||||
LookupChain lookupChain = lookupChainThreadLocal.get();
|
||||
if (lookupChain == null) {
|
||||
lookupChain = new LookupChain();
|
||||
lookupChainThreadLocal.set(lookupChain);
|
||||
}
|
||||
|
||||
// Prepare for re-entrant calls, then ask each factory to create a type adapter.
|
||||
DeferredAdapter<T> deferredAdapter = new DeferredAdapter<>(cacheKey);
|
||||
deferredAdapters.add(deferredAdapter);
|
||||
boolean success = false;
|
||||
JsonAdapter<T> adapterFromCall = lookupChain.push(type, fieldName, cacheKey);
|
||||
try {
|
||||
if (adapterFromCall != null) return adapterFromCall;
|
||||
|
||||
// Ask each factory to create the JSON adapter.
|
||||
for (int i = 0, size = factories.size(); i < size; i++) {
|
||||
JsonAdapter<T> result = (JsonAdapter<T>) factories.get(i).create(type, annotations, this);
|
||||
if (result != null) {
|
||||
deferredAdapter.ready(result);
|
||||
synchronized (adapterCache) {
|
||||
adapterCache.put(cacheKey, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
deferredAdapters.remove(deferredAdapters.size() - 1);
|
||||
if (deferredAdapters.isEmpty()) {
|
||||
reentrantCalls.remove();
|
||||
}
|
||||
}
|
||||
if (result == null) continue;
|
||||
|
||||
throw new IllegalArgumentException("No JsonAdapter for " + type + " annotated " + annotations);
|
||||
// Success! Notify the LookupChain so it is cached and can be used by re-entrant calls.
|
||||
lookupChain.adapterFound(result);
|
||||
success = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(
|
||||
"No JsonAdapter for " + typeAnnotatedWithAnnotations(type, annotations));
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw lookupChain.exceptionWithLookupStack(e);
|
||||
} finally {
|
||||
lookupChain.pop(success);
|
||||
}
|
||||
}
|
||||
|
||||
@CheckReturnValue
|
||||
@SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters.
|
||||
public <T> JsonAdapter<T> nextAdapter(JsonAdapter.Factory skipPast, Type type,
|
||||
Set<? extends Annotation> annotations) {
|
||||
type = Types.canonicalize(type);
|
||||
if (annotations == null) throw new NullPointerException("annotations == null");
|
||||
|
||||
type = removeSubtypeWildcard(canonicalize(type));
|
||||
|
||||
int skipPastIndex = factories.indexOf(skipPast);
|
||||
if (skipPastIndex == -1) {
|
||||
|
@ -129,11 +170,11 @@ public final class Moshi {
|
|||
if (result != null) return result;
|
||||
}
|
||||
throw new IllegalArgumentException("No next JsonAdapter for "
|
||||
+ type + " annotated " + annotations);
|
||||
+ typeAnnotatedWithAnnotations(type, annotations));
|
||||
}
|
||||
|
||||
/** Returns a new builder containing all custom factories used by the current instance. */
|
||||
public Moshi.Builder newBuilder() {
|
||||
@CheckReturnValue public Moshi.Builder newBuilder() {
|
||||
int fullSize = factories.size();
|
||||
int tailSize = BUILT_IN_FACTORIES.size();
|
||||
List<JsonAdapter.Factory> customFactories = factories.subList(0, fullSize - tailSize);
|
||||
|
@ -154,7 +195,7 @@ public final class Moshi {
|
|||
if (jsonAdapter == null) throw new IllegalArgumentException("jsonAdapter == null");
|
||||
|
||||
return add(new JsonAdapter.Factory() {
|
||||
@Override public JsonAdapter<?> create(
|
||||
@Override public @Nullable JsonAdapter<?> create(
|
||||
Type targetType, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
return annotations.isEmpty() && Util.typesMatch(type, targetType) ? jsonAdapter : null;
|
||||
}
|
||||
|
@ -174,7 +215,7 @@ public final class Moshi {
|
|||
}
|
||||
|
||||
return add(new JsonAdapter.Factory() {
|
||||
@Override public JsonAdapter<?> create(
|
||||
@Override public @Nullable JsonAdapter<?> create(
|
||||
Type targetType, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
if (Util.typesMatch(type, targetType)
|
||||
&& annotations.size() == 1
|
||||
|
@ -202,40 +243,138 @@ public final class Moshi {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Moshi build() {
|
||||
@CheckReturnValue public Moshi build() {
|
||||
return new Moshi(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sometimes a type adapter factory depends on its own product; either directly or indirectly.
|
||||
* To make this work, we offer this type adapter stub while the final adapter is being computed.
|
||||
* When it is ready, we wire this to delegate to that finished adapter.
|
||||
* A possibly-reentrant chain of lookups for JSON adapters.
|
||||
*
|
||||
* <p>Typically this is necessary in self-referential object models, such as an {@code Employee}
|
||||
* class that has a {@code List<Employee>} field for an organization's management hierarchy.
|
||||
* <p>We keep track of the current stack of lookups: we may start by looking up the JSON adapter
|
||||
* for Employee, re-enter looking for the JSON adapter of HomeAddress, and re-enter again looking
|
||||
* up the JSON adapter of PostalCode. If any of these lookups fail we can provide a stack trace
|
||||
* with all of the lookups.
|
||||
*
|
||||
* <p>Sometimes a JSON adapter factory depends on its own product; either directly or indirectly.
|
||||
* To make this work, we offer a JSON adapter stub while the final adapter is being computed.
|
||||
* When it is ready, we wire the stub to that finished adapter. This is necessary in
|
||||
* self-referential object models, such as an {@code Employee} class that has a {@code
|
||||
* List<Employee>} field for an organization's management hierarchy.
|
||||
*
|
||||
* <p>This class defers putting any JSON adapters in the cache until the topmost JSON adapter has
|
||||
* successfully been computed. That way we don't pollute the cache with incomplete stubs, or
|
||||
* adapters that may transitively depend on incomplete stubs.
|
||||
*/
|
||||
private static class DeferredAdapter<T> extends JsonAdapter<T> {
|
||||
Object cacheKey;
|
||||
private JsonAdapter<T> delegate;
|
||||
final class LookupChain {
|
||||
final List<Lookup<?>> callLookups = new ArrayList<>();
|
||||
final Deque<Lookup<?>> stack = new ArrayDeque<>();
|
||||
boolean exceptionAnnotated;
|
||||
|
||||
DeferredAdapter(Object cacheKey) {
|
||||
/**
|
||||
* Returns a JSON adapter that was already created for this call, or null if this is the first
|
||||
* time in this call that the cache key has been requested in this call. This may return a
|
||||
* lookup that isn't yet ready if this lookup is reentrant.
|
||||
*/
|
||||
<T> JsonAdapter<T> push(Type type, @Nullable String fieldName, Object cacheKey) {
|
||||
// Try to find a lookup with the same key for the same call.
|
||||
for (int i = 0, size = callLookups.size(); i < size; i++) {
|
||||
Lookup<?> lookup = callLookups.get(i);
|
||||
if (lookup.cacheKey.equals(cacheKey)) {
|
||||
Lookup<T> hit = (Lookup<T>) lookup;
|
||||
stack.add(hit);
|
||||
return hit.adapter != null ? hit.adapter : hit;
|
||||
}
|
||||
}
|
||||
|
||||
// We might need to know about this cache key later in this call. Prepare for that.
|
||||
Lookup<Object> lookup = new Lookup<>(type, fieldName, cacheKey);
|
||||
callLookups.add(lookup);
|
||||
stack.add(lookup);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Sets the adapter result of the current lookup. */
|
||||
<T> void adapterFound(JsonAdapter<T> result) {
|
||||
Lookup<T> currentLookup = (Lookup<T>) stack.getLast();
|
||||
currentLookup.adapter = result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes the current lookup by removing a stack frame.
|
||||
*
|
||||
* @param success true if the adapter cache should be populated if this is the topmost lookup.
|
||||
*/
|
||||
void pop(boolean success) {
|
||||
stack.removeLast();
|
||||
if (!stack.isEmpty()) return;
|
||||
|
||||
lookupChainThreadLocal.remove();
|
||||
|
||||
if (success) {
|
||||
synchronized (adapterCache) {
|
||||
for (int i = 0, size = callLookups.size(); i < size; i++) {
|
||||
Lookup<?> lookup = callLookups.get(i);
|
||||
JsonAdapter<?> replaced = adapterCache.put(lookup.cacheKey, lookup.adapter);
|
||||
if (replaced != null) {
|
||||
((Lookup<Object>) lookup).adapter = (JsonAdapter<Object>) replaced;
|
||||
adapterCache.put(lookup.cacheKey, replaced);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IllegalArgumentException exceptionWithLookupStack(IllegalArgumentException e) {
|
||||
// Don't add the lookup stack to more than one exception; the deepest is sufficient.
|
||||
if (exceptionAnnotated) return e;
|
||||
exceptionAnnotated = true;
|
||||
|
||||
int size = stack.size();
|
||||
if (size == 1 && stack.getFirst().fieldName == null) return e;
|
||||
|
||||
StringBuilder errorMessageBuilder = new StringBuilder(e.getMessage());
|
||||
for (Iterator<Lookup<?>> i = stack.descendingIterator(); i.hasNext(); ) {
|
||||
Lookup<?> lookup = i.next();
|
||||
errorMessageBuilder
|
||||
.append("\nfor ")
|
||||
.append(lookup.type);
|
||||
if (lookup.fieldName != null) {
|
||||
errorMessageBuilder
|
||||
.append(' ')
|
||||
.append(lookup.fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
return new IllegalArgumentException(errorMessageBuilder.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/** This class implements {@code JsonAdapter} so it can be used as a stub for re-entrant calls. */
|
||||
static final class Lookup<T> extends JsonAdapter<T> {
|
||||
final Type type;
|
||||
final @Nullable String fieldName;
|
||||
final Object cacheKey;
|
||||
@Nullable JsonAdapter<T> adapter;
|
||||
|
||||
Lookup(Type type, @Nullable String fieldName, Object cacheKey) {
|
||||
this.type = type;
|
||||
this.fieldName = fieldName;
|
||||
this.cacheKey = cacheKey;
|
||||
}
|
||||
|
||||
void ready(JsonAdapter<T> delegate) {
|
||||
this.delegate = delegate;
|
||||
this.cacheKey = null;
|
||||
}
|
||||
|
||||
@Override public T fromJson(JsonReader reader) throws IOException {
|
||||
if (delegate == null) throw new IllegalStateException("Type adapter isn't ready");
|
||||
return delegate.fromJson(reader);
|
||||
if (adapter == null) throw new IllegalStateException("JsonAdapter isn't ready");
|
||||
return adapter.fromJson(reader);
|
||||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, T value) throws IOException {
|
||||
if (delegate == null) throw new IllegalStateException("Type adapter isn't ready");
|
||||
delegate.toJson(writer, value);
|
||||
if (adapter == null) throw new IllegalStateException("JsonAdapter isn't ready");
|
||||
adapter.toJson(writer, value);
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
return adapter != null ? adapter.toString() : super.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,15 +15,23 @@
|
|||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import com.squareup.moshi.internal.Util;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static com.squareup.moshi.internal.Util.generatedAdapter;
|
||||
|
||||
final class StandardJsonAdapters {
|
||||
private StandardJsonAdapters() {
|
||||
}
|
||||
|
||||
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
|
||||
@Override public JsonAdapter<?> create(
|
||||
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
|
@ -48,6 +56,12 @@ final class StandardJsonAdapters {
|
|||
if (type == Object.class) return new ObjectJsonAdapter(moshi).nullSafe();
|
||||
|
||||
Class<?> rawType = Types.getRawType(type);
|
||||
|
||||
@Nullable JsonAdapter<?> generatedAdapter = generatedAdapter(moshi, type, rawType);
|
||||
if (generatedAdapter != null) {
|
||||
return generatedAdapter;
|
||||
}
|
||||
|
||||
if (rawType.isEnum()) {
|
||||
//noinspection unchecked
|
||||
return new EnumJsonAdapter<>((Class<? extends Enum>) rawType).nullSafe();
|
||||
|
@ -216,7 +230,7 @@ final class StandardJsonAdapters {
|
|||
private final T[] constants;
|
||||
private final JsonReader.Options options;
|
||||
|
||||
public EnumJsonAdapter(Class<T> enumType) {
|
||||
EnumJsonAdapter(Class<T> enumType) {
|
||||
this.enumType = enumType;
|
||||
try {
|
||||
constants = enumType.getEnumConstants();
|
||||
|
@ -238,10 +252,10 @@ final class StandardJsonAdapters {
|
|||
if (index != -1) return constants[index];
|
||||
|
||||
// We can consume the string safely, we are terminating anyway.
|
||||
String path = reader.getPath();
|
||||
String name = reader.nextString();
|
||||
throw new JsonDataException("Expected one of "
|
||||
+ Arrays.asList(nameStrings) + " but was " + name + " at path "
|
||||
+ reader.getPath());
|
||||
+ Arrays.asList(nameStrings) + " but was " + name + " at path " + path);
|
||||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, T value) throws IOException {
|
||||
|
@ -263,13 +277,45 @@ final class StandardJsonAdapters {
|
|||
*/
|
||||
static final class ObjectJsonAdapter extends JsonAdapter<Object> {
|
||||
private final Moshi moshi;
|
||||
private final JsonAdapter<List> listJsonAdapter;
|
||||
private final JsonAdapter<Map> mapAdapter;
|
||||
private final JsonAdapter<String> stringAdapter;
|
||||
private final JsonAdapter<Double> doubleAdapter;
|
||||
private final JsonAdapter<Boolean> booleanAdapter;
|
||||
|
||||
public ObjectJsonAdapter(Moshi moshi) {
|
||||
ObjectJsonAdapter(Moshi moshi) {
|
||||
this.moshi = moshi;
|
||||
this.listJsonAdapter = moshi.adapter(List.class);
|
||||
this.mapAdapter = moshi.adapter(Map.class);
|
||||
this.stringAdapter = moshi.adapter(String.class);
|
||||
this.doubleAdapter = moshi.adapter(Double.class);
|
||||
this.booleanAdapter = moshi.adapter(Boolean.class);
|
||||
}
|
||||
|
||||
@Override public Object fromJson(JsonReader reader) throws IOException {
|
||||
return reader.readJsonValue();
|
||||
switch (reader.peek()) {
|
||||
case BEGIN_ARRAY:
|
||||
return listJsonAdapter.fromJson(reader);
|
||||
|
||||
case BEGIN_OBJECT:
|
||||
return mapAdapter.fromJson(reader);
|
||||
|
||||
case STRING:
|
||||
return stringAdapter.fromJson(reader);
|
||||
|
||||
case NUMBER:
|
||||
return doubleAdapter.fromJson(reader);
|
||||
|
||||
case BOOLEAN:
|
||||
return booleanAdapter.fromJson(reader);
|
||||
|
||||
case NULL:
|
||||
return reader.nextNull();
|
||||
|
||||
default:
|
||||
throw new IllegalStateException(
|
||||
"Expected a value but was " + reader.peek() + " at path " + reader.getPath());
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, Object value) throws IOException {
|
||||
|
|
|
@ -15,10 +15,13 @@
|
|||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import com.squareup.moshi.internal.Util.GenericArrayTypeImpl;
|
||||
import com.squareup.moshi.internal.Util.ParameterizedTypeImpl;
|
||||
import com.squareup.moshi.internal.Util.WildcardTypeImpl;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.GenericArrayType;
|
||||
import java.lang.reflect.GenericDeclaration;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
|
@ -31,23 +34,59 @@ import java.util.Collection;
|
|||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static com.squareup.moshi.internal.Util.EMPTY_TYPE_ARRAY;
|
||||
import static com.squareup.moshi.internal.Util.getGenericSupertype;
|
||||
import static com.squareup.moshi.internal.Util.resolve;
|
||||
|
||||
/** Factory methods for types. */
|
||||
@CheckReturnValue
|
||||
public final class Types {
|
||||
static final Type[] EMPTY_TYPE_ARRAY = new Type[] {};
|
||||
|
||||
private Types() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the generated {@link JsonAdapter} fully qualified class name for a given
|
||||
* {@link JsonClass JsonClass-annotated} {@code clazz}. This is the same lookup logic used by
|
||||
* both the Moshi code generation as well as lookup for any JsonClass-annotated classes. This can
|
||||
* be useful if generating your own JsonAdapters without using Moshi's first party code gen.
|
||||
*
|
||||
* @param clazz the class to calculate a generated JsonAdapter name for.
|
||||
* @return the resolved fully qualified class name to the expected generated JsonAdapter class.
|
||||
* Note that this name will always be a top-level class name and not a nested class.
|
||||
*/
|
||||
public static String generatedJsonAdapterName(Class<?> clazz) {
|
||||
if (clazz.getAnnotation(JsonClass.class) == null) {
|
||||
throw new IllegalArgumentException("Class does not have a JsonClass annotation: " + clazz);
|
||||
}
|
||||
return generatedJsonAdapterName(clazz.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the generated {@link JsonAdapter} fully qualified class name for a given
|
||||
* {@link JsonClass JsonClass-annotated} {@code className}. This is the same lookup logic used by
|
||||
* both the Moshi code generation as well as lookup for any JsonClass-annotated classes. This can
|
||||
* be useful if generating your own JsonAdapters without using Moshi's first party code gen.
|
||||
*
|
||||
* @param className the fully qualified class to calculate a generated JsonAdapter name for.
|
||||
* @return the resolved fully qualified class name to the expected generated JsonAdapter class.
|
||||
* Note that this name will always be a top-level class name and not a nested class.
|
||||
*/
|
||||
public static String generatedJsonAdapterName(String className) {
|
||||
return className.replace("$", "_") + "JsonAdapter";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if {@code annotations} contains {@code jsonQualifier}.
|
||||
* Returns the subset of {@code annotations} without {@code jsonQualifier}, or null if {@code
|
||||
* annotations} does not contain {@code jsonQualifier}.
|
||||
*/
|
||||
public static Set<? extends Annotation> nextAnnotations(Set<? extends Annotation> annotations,
|
||||
public static @Nullable Set<? extends Annotation> nextAnnotations(
|
||||
Set<? extends Annotation> annotations,
|
||||
Class<? extends Annotation> jsonQualifier) {
|
||||
if (!jsonQualifier.isAnnotationPresent(JsonQualifier.class)) {
|
||||
throw new IllegalArgumentException(jsonQualifier + " is not a JsonQualifier.");
|
||||
|
@ -57,7 +96,7 @@ public final class Types {
|
|||
}
|
||||
for (Annotation annotation : annotations) {
|
||||
if (jsonQualifier.equals(annotation.annotationType())) {
|
||||
Set<Annotation> delegateAnnotations = new LinkedHashSet<>(annotations);
|
||||
Set<? extends Annotation> delegateAnnotations = new LinkedHashSet<>(annotations);
|
||||
delegateAnnotations.remove(annotation);
|
||||
return Collections.unmodifiableSet(delegateAnnotations);
|
||||
}
|
||||
|
@ -105,36 +144,6 @@ public final class Types {
|
|||
return new WildcardTypeImpl(new Type[] { Object.class }, new Type[] { bound });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a type that is functionally equal but not necessarily equal according to {@link
|
||||
* Object#equals(Object) Object.equals()}.
|
||||
*/
|
||||
static Type canonicalize(Type type) {
|
||||
if (type instanceof Class) {
|
||||
Class<?> c = (Class<?>) type;
|
||||
return c.isArray() ? new GenericArrayTypeImpl(canonicalize(c.getComponentType())) : c;
|
||||
|
||||
} else if (type instanceof ParameterizedType) {
|
||||
if (type instanceof ParameterizedTypeImpl) return type;
|
||||
ParameterizedType p = (ParameterizedType) type;
|
||||
return new ParameterizedTypeImpl(p.getOwnerType(),
|
||||
p.getRawType(), p.getActualTypeArguments());
|
||||
|
||||
} else if (type instanceof GenericArrayType) {
|
||||
if (type instanceof GenericArrayTypeImpl) return type;
|
||||
GenericArrayType g = (GenericArrayType) type;
|
||||
return new GenericArrayTypeImpl(g.getGenericComponentType());
|
||||
|
||||
} else if (type instanceof WildcardType) {
|
||||
if (type instanceof WildcardTypeImpl) return type;
|
||||
WildcardType w = (WildcardType) type;
|
||||
return new WildcardTypeImpl(w.getUpperBounds(), w.getLowerBounds());
|
||||
|
||||
} else {
|
||||
return type; // This type is unsupported!
|
||||
}
|
||||
}
|
||||
|
||||
public static Class<?> getRawType(Type type) {
|
||||
if (type instanceof Class<?>) {
|
||||
// type is a normal class.
|
||||
|
@ -149,7 +158,7 @@ public final class Types {
|
|||
return (Class<?>) rawType;
|
||||
|
||||
} else if (type instanceof GenericArrayType) {
|
||||
Type componentType = ((GenericArrayType)type).getGenericComponentType();
|
||||
Type componentType = ((GenericArrayType) type).getGenericComponentType();
|
||||
return Array.newInstance(getRawType(componentType), 0).getClass();
|
||||
|
||||
} else if (type instanceof TypeVariable) {
|
||||
|
@ -167,6 +176,106 @@ public final class Types {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the element type of this collection type.
|
||||
* @throws IllegalArgumentException if this type is not a collection.
|
||||
*/
|
||||
public static Type collectionElementType(Type context, Class<?> contextRawType) {
|
||||
Type collectionType = getSupertype(context, contextRawType, Collection.class);
|
||||
|
||||
if (collectionType instanceof WildcardType) {
|
||||
collectionType = ((WildcardType) collectionType).getUpperBounds()[0];
|
||||
}
|
||||
if (collectionType instanceof ParameterizedType) {
|
||||
return ((ParameterizedType) collectionType).getActualTypeArguments()[0];
|
||||
}
|
||||
return Object.class;
|
||||
}
|
||||
|
||||
/** Returns true if {@code a} and {@code b} are equal. */
|
||||
public static boolean equals(@Nullable Type a, @Nullable Type b) {
|
||||
if (a == b) {
|
||||
return true; // Also handles (a == null && b == null).
|
||||
|
||||
} else if (a instanceof Class) {
|
||||
if (b instanceof GenericArrayType) {
|
||||
return equals(((Class) a).getComponentType(),
|
||||
((GenericArrayType) b).getGenericComponentType());
|
||||
}
|
||||
return a.equals(b); // Class already specifies equals().
|
||||
|
||||
} else if (a instanceof ParameterizedType) {
|
||||
if (!(b instanceof ParameterizedType)) return false;
|
||||
ParameterizedType pa = (ParameterizedType) a;
|
||||
ParameterizedType pb = (ParameterizedType) b;
|
||||
Type[] aTypeArguments = pa instanceof ParameterizedTypeImpl
|
||||
? ((ParameterizedTypeImpl) pa).typeArguments
|
||||
: pa.getActualTypeArguments();
|
||||
Type[] bTypeArguments = pb instanceof ParameterizedTypeImpl
|
||||
? ((ParameterizedTypeImpl) pb).typeArguments
|
||||
: pb.getActualTypeArguments();
|
||||
return equals(pa.getOwnerType(), pb.getOwnerType())
|
||||
&& pa.getRawType().equals(pb.getRawType())
|
||||
&& Arrays.equals(aTypeArguments, bTypeArguments);
|
||||
|
||||
} else if (a instanceof GenericArrayType) {
|
||||
if (b instanceof Class) {
|
||||
return equals(((Class) b).getComponentType(),
|
||||
((GenericArrayType) a).getGenericComponentType());
|
||||
}
|
||||
if (!(b instanceof GenericArrayType)) return false;
|
||||
GenericArrayType ga = (GenericArrayType) a;
|
||||
GenericArrayType gb = (GenericArrayType) b;
|
||||
return equals(ga.getGenericComponentType(), gb.getGenericComponentType());
|
||||
|
||||
} else if (a instanceof WildcardType) {
|
||||
if (!(b instanceof WildcardType)) return false;
|
||||
WildcardType wa = (WildcardType) a;
|
||||
WildcardType wb = (WildcardType) b;
|
||||
return Arrays.equals(wa.getUpperBounds(), wb.getUpperBounds())
|
||||
&& Arrays.equals(wa.getLowerBounds(), wb.getLowerBounds());
|
||||
|
||||
} else if (a instanceof TypeVariable) {
|
||||
if (!(b instanceof TypeVariable)) return false;
|
||||
TypeVariable<?> va = (TypeVariable<?>) a;
|
||||
TypeVariable<?> vb = (TypeVariable<?>) b;
|
||||
return va.getGenericDeclaration() == vb.getGenericDeclaration()
|
||||
&& va.getName().equals(vb.getName());
|
||||
|
||||
} else {
|
||||
// This isn't a supported type.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param clazz the target class to read the {@code fieldName} field annotations from.
|
||||
* @param fieldName the target field name on {@code clazz}.
|
||||
* @return a set of {@link JsonQualifier}-annotated {@link Annotation} instances retrieved from
|
||||
* the targeted field. Can be empty if none are found.
|
||||
*/
|
||||
public static Set<? extends Annotation> getFieldJsonQualifierAnnotations(Class<?> clazz,
|
||||
String fieldName) {
|
||||
try {
|
||||
Field field = clazz.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
Annotation[] fieldAnnotations = field.getDeclaredAnnotations();
|
||||
Set<Annotation> annotations = new LinkedHashSet<>(fieldAnnotations.length);
|
||||
for (Annotation annotation : fieldAnnotations) {
|
||||
if (annotation.annotationType().isAnnotationPresent(JsonQualifier.class)) {
|
||||
annotations.add(annotation);
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableSet(annotations);
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new IllegalArgumentException("Could not access field "
|
||||
+ fieldName
|
||||
+ " on class "
|
||||
+ clazz.getCanonicalName(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T extends Annotation> T createJsonQualifierImplementation(final Class<T> annotationType) {
|
||||
if (!annotationType.isAnnotation()) {
|
||||
|
@ -200,103 +309,21 @@ public final class Types {
|
|||
});
|
||||
}
|
||||
|
||||
static boolean equal(Object a, Object b) {
|
||||
return a == b || (a != null && a.equals(b));
|
||||
}
|
||||
|
||||
/** Returns true if {@code a} and {@code b} are equal. */
|
||||
static boolean equals(Type a, Type b) {
|
||||
if (a == b) {
|
||||
return true; // Also handles (a == null && b == null).
|
||||
|
||||
} else if (a instanceof Class) {
|
||||
return a.equals(b); // Class already specifies equals().
|
||||
|
||||
} else if (a instanceof ParameterizedType) {
|
||||
if (!(b instanceof ParameterizedType)) return false;
|
||||
ParameterizedType pa = (ParameterizedType) a;
|
||||
ParameterizedType pb = (ParameterizedType) b;
|
||||
Type[] aTypeArguments = pa instanceof ParameterizedTypeImpl
|
||||
? ((ParameterizedTypeImpl) pa).typeArguments
|
||||
: pa.getActualTypeArguments();
|
||||
Type[] bTypeArguments = pb instanceof ParameterizedTypeImpl
|
||||
? ((ParameterizedTypeImpl) pb).typeArguments
|
||||
: pb.getActualTypeArguments();
|
||||
return equal(pa.getOwnerType(), pb.getOwnerType())
|
||||
&& pa.getRawType().equals(pb.getRawType())
|
||||
&& Arrays.equals(aTypeArguments, bTypeArguments);
|
||||
|
||||
} else if (a instanceof GenericArrayType) {
|
||||
if (!(b instanceof GenericArrayType)) return false;
|
||||
GenericArrayType ga = (GenericArrayType) a;
|
||||
GenericArrayType gb = (GenericArrayType) b;
|
||||
return equals(ga.getGenericComponentType(), gb.getGenericComponentType());
|
||||
|
||||
} else if (a instanceof WildcardType) {
|
||||
if (!(b instanceof WildcardType)) return false;
|
||||
WildcardType wa = (WildcardType) a;
|
||||
WildcardType wb = (WildcardType) b;
|
||||
return Arrays.equals(wa.getUpperBounds(), wb.getUpperBounds())
|
||||
&& Arrays.equals(wa.getLowerBounds(), wb.getLowerBounds());
|
||||
|
||||
} else if (a instanceof TypeVariable) {
|
||||
if (!(b instanceof TypeVariable)) return false;
|
||||
TypeVariable<?> va = (TypeVariable<?>) a;
|
||||
TypeVariable<?> vb = (TypeVariable<?>) b;
|
||||
return va.getGenericDeclaration() == vb.getGenericDeclaration()
|
||||
&& va.getName().equals(vb.getName());
|
||||
|
||||
} else {
|
||||
// This isn't a supported type.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static int hashCodeOrZero(Object o) {
|
||||
return o != null ? o.hashCode() : 0;
|
||||
}
|
||||
|
||||
static String typeToString(Type type) {
|
||||
return type instanceof Class ? ((Class<?>) type).getName() : type.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the generic supertype for {@code supertype}. For example, given a class {@code
|
||||
* IntegerSet}, the result for when supertype is {@code Set.class} is {@code Set<Integer>} and the
|
||||
* result when the supertype is {@code Collection.class} is {@code Collection<Integer>}.
|
||||
* Returns a two element array containing this map's key and value types in positions 0 and 1
|
||||
* respectively.
|
||||
*/
|
||||
static Type getGenericSupertype(Type context, Class<?> rawType, Class<?> toResolve) {
|
||||
if (toResolve == rawType) {
|
||||
return context;
|
||||
}
|
||||
static Type[] mapKeyAndValueTypes(Type context, Class<?> contextRawType) {
|
||||
// Work around a problem with the declaration of java.util.Properties. That class should extend
|
||||
// Hashtable<String, String>, but it's declared to extend Hashtable<Object, Object>.
|
||||
if (context == Properties.class) return new Type[] { String.class, String.class };
|
||||
|
||||
// we skip searching through interfaces if unknown is an interface
|
||||
if (toResolve.isInterface()) {
|
||||
Class<?>[] interfaces = rawType.getInterfaces();
|
||||
for (int i = 0, length = interfaces.length; i < length; i++) {
|
||||
if (interfaces[i] == toResolve) {
|
||||
return rawType.getGenericInterfaces()[i];
|
||||
} else if (toResolve.isAssignableFrom(interfaces[i])) {
|
||||
return getGenericSupertype(rawType.getGenericInterfaces()[i], interfaces[i], toResolve);
|
||||
}
|
||||
}
|
||||
Type mapType = getSupertype(context, contextRawType, Map.class);
|
||||
if (mapType instanceof ParameterizedType) {
|
||||
ParameterizedType mapParameterizedType = (ParameterizedType) mapType;
|
||||
return mapParameterizedType.getActualTypeArguments();
|
||||
}
|
||||
|
||||
// check our supertypes
|
||||
if (!rawType.isInterface()) {
|
||||
while (rawType != Object.class) {
|
||||
Class<?> rawSupertype = rawType.getSuperclass();
|
||||
if (rawSupertype == toResolve) {
|
||||
return rawType.getGenericSuperclass();
|
||||
} else if (toResolve.isAssignableFrom(rawSupertype)) {
|
||||
return getGenericSupertype(rawType.getGenericSuperclass(), rawSupertype, toResolve);
|
||||
}
|
||||
rawType = rawSupertype;
|
||||
}
|
||||
}
|
||||
|
||||
// we can't resolve this further
|
||||
return toResolve;
|
||||
return new Type[] { Object.class, Object.class };
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -330,305 +357,4 @@ public final class Types {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the element type of this collection type.
|
||||
* @throws IllegalArgumentException if this type is not a collection.
|
||||
*/
|
||||
public static Type collectionElementType(Type context, Class<?> contextRawType) {
|
||||
Type collectionType = getSupertype(context, contextRawType, Collection.class);
|
||||
|
||||
if (collectionType instanceof WildcardType) {
|
||||
collectionType = ((WildcardType) collectionType).getUpperBounds()[0];
|
||||
}
|
||||
if (collectionType instanceof ParameterizedType) {
|
||||
return ((ParameterizedType) collectionType).getActualTypeArguments()[0];
|
||||
}
|
||||
return Object.class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a two element array containing this map's key and value types in positions 0 and 1
|
||||
* respectively.
|
||||
*/
|
||||
static Type[] mapKeyAndValueTypes(Type context, Class<?> contextRawType) {
|
||||
// Work around a problem with the declaration of java.util.Properties. That class should extend
|
||||
// Hashtable<String, String>, but it's declared to extend Hashtable<Object, Object>.
|
||||
if (context == Properties.class) return new Type[] { String.class, String.class };
|
||||
|
||||
Type mapType = getSupertype(context, contextRawType, Map.class);
|
||||
if (mapType instanceof ParameterizedType) {
|
||||
ParameterizedType mapParameterizedType = (ParameterizedType) mapType;
|
||||
return mapParameterizedType.getActualTypeArguments();
|
||||
}
|
||||
return new Type[] { Object.class, Object.class };
|
||||
}
|
||||
|
||||
static Type resolve(Type context, Class<?> contextRawType, Type toResolve) {
|
||||
// This implementation is made a little more complicated in an attempt to avoid object-creation.
|
||||
while (true) {
|
||||
if (toResolve instanceof TypeVariable) {
|
||||
TypeVariable<?> typeVariable = (TypeVariable<?>) toResolve;
|
||||
toResolve = resolveTypeVariable(context, contextRawType, typeVariable);
|
||||
if (toResolve == typeVariable) return toResolve;
|
||||
|
||||
} else if (toResolve instanceof Class && ((Class<?>) toResolve).isArray()) {
|
||||
Class<?> original = (Class<?>) toResolve;
|
||||
Type componentType = original.getComponentType();
|
||||
Type newComponentType = resolve(context, contextRawType, componentType);
|
||||
return componentType == newComponentType
|
||||
? original
|
||||
: arrayOf(newComponentType);
|
||||
|
||||
} else if (toResolve instanceof GenericArrayType) {
|
||||
GenericArrayType original = (GenericArrayType) toResolve;
|
||||
Type componentType = original.getGenericComponentType();
|
||||
Type newComponentType = resolve(context, contextRawType, componentType);
|
||||
return componentType == newComponentType
|
||||
? original
|
||||
: arrayOf(newComponentType);
|
||||
|
||||
} else if (toResolve instanceof ParameterizedType) {
|
||||
ParameterizedType original = (ParameterizedType) toResolve;
|
||||
Type ownerType = original.getOwnerType();
|
||||
Type newOwnerType = resolve(context, contextRawType, ownerType);
|
||||
boolean changed = newOwnerType != ownerType;
|
||||
|
||||
Type[] args = original.getActualTypeArguments();
|
||||
for (int t = 0, length = args.length; t < length; t++) {
|
||||
Type resolvedTypeArgument = resolve(context, contextRawType, args[t]);
|
||||
if (resolvedTypeArgument != args[t]) {
|
||||
if (!changed) {
|
||||
args = args.clone();
|
||||
changed = true;
|
||||
}
|
||||
args[t] = resolvedTypeArgument;
|
||||
}
|
||||
}
|
||||
|
||||
return changed
|
||||
? new ParameterizedTypeImpl(newOwnerType, original.getRawType(), args)
|
||||
: original;
|
||||
|
||||
} else if (toResolve instanceof WildcardType) {
|
||||
WildcardType original = (WildcardType) toResolve;
|
||||
Type[] originalLowerBound = original.getLowerBounds();
|
||||
Type[] originalUpperBound = original.getUpperBounds();
|
||||
|
||||
if (originalLowerBound.length == 1) {
|
||||
Type lowerBound = resolve(context, contextRawType, originalLowerBound[0]);
|
||||
if (lowerBound != originalLowerBound[0]) {
|
||||
return supertypeOf(lowerBound);
|
||||
}
|
||||
} else if (originalUpperBound.length == 1) {
|
||||
Type upperBound = resolve(context, contextRawType, originalUpperBound[0]);
|
||||
if (upperBound != originalUpperBound[0]) {
|
||||
return subtypeOf(upperBound);
|
||||
}
|
||||
}
|
||||
return original;
|
||||
|
||||
} else {
|
||||
return toResolve;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Type resolveTypeVariable(Type context, Class<?> contextRawType, TypeVariable<?> unknown) {
|
||||
Class<?> declaredByRaw = declaringClassOf(unknown);
|
||||
|
||||
// We can't reduce this further.
|
||||
if (declaredByRaw == null) return unknown;
|
||||
|
||||
Type declaredBy = getGenericSupertype(context, contextRawType, declaredByRaw);
|
||||
if (declaredBy instanceof ParameterizedType) {
|
||||
int index = indexOf(declaredByRaw.getTypeParameters(), unknown);
|
||||
return ((ParameterizedType) declaredBy).getActualTypeArguments()[index];
|
||||
}
|
||||
|
||||
return unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this is a Type supported by {@link StandardJsonAdapters#FACTORY}.
|
||||
*/
|
||||
static boolean isAllowedPlatformType(Type type) {
|
||||
return type == Boolean.class
|
||||
|| type == Byte.class
|
||||
|| type == Character.class
|
||||
|| type == Double.class
|
||||
|| type == Float.class
|
||||
|| type == Integer.class
|
||||
|| type == Long.class
|
||||
|| type == Short.class
|
||||
|| type == String.class
|
||||
|| type == Object.class;
|
||||
}
|
||||
|
||||
private static int indexOf(Object[] array, Object toFind) {
|
||||
for (int i = 0; i < array.length; i++) {
|
||||
if (toFind.equals(array[i])) return i;
|
||||
}
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the declaring class of {@code typeVariable}, or {@code null} if it was not declared by
|
||||
* a class.
|
||||
*/
|
||||
private static Class<?> declaringClassOf(TypeVariable<?> typeVariable) {
|
||||
GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration();
|
||||
return genericDeclaration instanceof Class ? (Class<?>) genericDeclaration : null;
|
||||
}
|
||||
|
||||
static void checkNotPrimitive(Type type) {
|
||||
if ((type instanceof Class<?>) && ((Class<?>) type).isPrimitive()) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ParameterizedTypeImpl implements ParameterizedType {
|
||||
private final Type ownerType;
|
||||
private final Type rawType;
|
||||
final Type[] typeArguments;
|
||||
|
||||
ParameterizedTypeImpl(Type ownerType, Type rawType, Type... typeArguments) {
|
||||
// Require an owner type if the raw type needs it.
|
||||
if (rawType instanceof Class<?>
|
||||
&& (ownerType == null) != (((Class<?>) rawType).getEnclosingClass() == null)) {
|
||||
throw new IllegalArgumentException(
|
||||
"unexpected owner type for " + rawType + ": " + ownerType);
|
||||
}
|
||||
|
||||
this.ownerType = ownerType == null ? null : canonicalize(ownerType);
|
||||
this.rawType = canonicalize(rawType);
|
||||
this.typeArguments = typeArguments.clone();
|
||||
for (int t = 0; t < this.typeArguments.length; t++) {
|
||||
if (this.typeArguments[t] == null) throw new NullPointerException();
|
||||
checkNotPrimitive(this.typeArguments[t]);
|
||||
this.typeArguments[t] = canonicalize(this.typeArguments[t]);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public Type[] getActualTypeArguments() {
|
||||
return typeArguments.clone();
|
||||
}
|
||||
|
||||
@Override public Type getRawType() {
|
||||
return rawType;
|
||||
}
|
||||
|
||||
@Override public Type getOwnerType() {
|
||||
return ownerType;
|
||||
}
|
||||
|
||||
@Override public boolean equals(Object other) {
|
||||
return other instanceof ParameterizedType
|
||||
&& Types.equals(this, (ParameterizedType) other);
|
||||
}
|
||||
|
||||
@Override public int hashCode() {
|
||||
return Arrays.hashCode(typeArguments)
|
||||
^ rawType.hashCode()
|
||||
^ hashCodeOrZero(ownerType);
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
StringBuilder result = new StringBuilder(30 * (typeArguments.length + 1));
|
||||
result.append(typeToString(rawType));
|
||||
|
||||
if (typeArguments.length == 0) {
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
result.append("<").append(typeToString(typeArguments[0]));
|
||||
for (int i = 1; i < typeArguments.length; i++) {
|
||||
result.append(", ").append(typeToString(typeArguments[i]));
|
||||
}
|
||||
return result.append(">").toString();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class GenericArrayTypeImpl implements GenericArrayType {
|
||||
private final Type componentType;
|
||||
|
||||
public GenericArrayTypeImpl(Type componentType) {
|
||||
this.componentType = canonicalize(componentType);
|
||||
}
|
||||
|
||||
@Override public Type getGenericComponentType() {
|
||||
return componentType;
|
||||
}
|
||||
|
||||
@Override public boolean equals(Object o) {
|
||||
return o instanceof GenericArrayType
|
||||
&& Types.equals(this, (GenericArrayType) o);
|
||||
}
|
||||
|
||||
@Override public int hashCode() {
|
||||
return componentType.hashCode();
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
return typeToString(componentType) + "[]";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The WildcardType interface supports multiple upper bounds and multiple lower bounds. We only
|
||||
* support what the Java 6 language needs - at most one bound. If a lower bound is set, the upper
|
||||
* bound must be Object.class.
|
||||
*/
|
||||
private static final class WildcardTypeImpl implements WildcardType {
|
||||
private final Type upperBound;
|
||||
private final Type lowerBound;
|
||||
|
||||
public WildcardTypeImpl(Type[] upperBounds, Type[] lowerBounds) {
|
||||
if (lowerBounds.length > 1) throw new IllegalArgumentException();
|
||||
if (upperBounds.length != 1) throw new IllegalArgumentException();
|
||||
|
||||
if (lowerBounds.length == 1) {
|
||||
if (lowerBounds[0] == null) throw new NullPointerException();
|
||||
checkNotPrimitive(lowerBounds[0]);
|
||||
if (upperBounds[0] != Object.class) throw new IllegalArgumentException();
|
||||
this.lowerBound = canonicalize(lowerBounds[0]);
|
||||
this.upperBound = Object.class;
|
||||
|
||||
} else {
|
||||
if (upperBounds[0] == null) throw new NullPointerException();
|
||||
checkNotPrimitive(upperBounds[0]);
|
||||
this.lowerBound = null;
|
||||
this.upperBound = canonicalize(upperBounds[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public Type[] getUpperBounds() {
|
||||
return new Type[] { upperBound };
|
||||
}
|
||||
|
||||
@Override public Type[] getLowerBounds() {
|
||||
return lowerBound != null ? new Type[] { lowerBound } : EMPTY_TYPE_ARRAY;
|
||||
}
|
||||
|
||||
@Override public boolean equals(Object other) {
|
||||
return other instanceof WildcardType
|
||||
&& Types.equals(this, (WildcardType) other);
|
||||
}
|
||||
|
||||
@Override public int hashCode() {
|
||||
// This equals Arrays.hashCode(getLowerBounds()) ^ Arrays.hashCode(getUpperBounds()).
|
||||
return (lowerBound != null ? 31 + lowerBound.hashCode() : 1)
|
||||
^ (31 + upperBound.hashCode());
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
if (lowerBound != null) {
|
||||
return "? super " + typeToString(lowerBound);
|
||||
} else if (upperBound == Object.class) {
|
||||
return "?";
|
||||
} else {
|
||||
return "? extends " + typeToString(upperBound);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2014 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
final class Util {
|
||||
public static final Set<Annotation> NO_ANNOTATIONS = Collections.emptySet();
|
||||
|
||||
public static boolean typesMatch(Type pattern, Type candidate) {
|
||||
// TODO: permit raw types (like Set.class) to match non-raw candidates (like Set<Long>).
|
||||
return pattern.equals(candidate);
|
||||
}
|
||||
|
||||
public static Set<? extends Annotation> jsonAnnotations(AnnotatedElement annotatedElement) {
|
||||
return jsonAnnotations(annotatedElement.getAnnotations());
|
||||
}
|
||||
|
||||
public static Set<? extends Annotation> jsonAnnotations(Annotation[] annotations) {
|
||||
Set<Annotation> result = null;
|
||||
for (Annotation annotation : annotations) {
|
||||
if (annotation.annotationType().isAnnotationPresent(JsonQualifier.class)) {
|
||||
if (result == null) result = new LinkedHashSet<>();
|
||||
result.add(annotation);
|
||||
}
|
||||
}
|
||||
return result != null ? Collections.unmodifiableSet(result) : Util.NO_ANNOTATIONS;
|
||||
}
|
||||
|
||||
public static boolean isAnnotationPresent(
|
||||
Set<? extends Annotation> annotations, Class<? extends Annotation> annotationClass) {
|
||||
if (annotations.isEmpty()) return false; // Save an iterator in the common case.
|
||||
for (Annotation annotation : annotations) {
|
||||
if (annotation.annotationType() == annotationClass) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Returns true if {@code annotations} has any annotation whose simple name is Nullable. */
|
||||
public static boolean hasNullable(Annotation[] annotations) {
|
||||
for (Annotation annotation : annotations) {
|
||||
if (annotation.annotationType().getSimpleName().equals("Nullable")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (C) 2019 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.internal;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.JsonWriter;
|
||||
import java.io.IOException;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public final class NullSafeJsonAdapter<T> extends JsonAdapter<T> {
|
||||
|
||||
private final JsonAdapter<T> delegate;
|
||||
|
||||
public NullSafeJsonAdapter(JsonAdapter<T> delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
public JsonAdapter<T> delegate() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
@Override public @Nullable T fromJson(JsonReader reader) throws IOException {
|
||||
if (reader.peek() == JsonReader.Token.NULL) {
|
||||
return reader.nextNull();
|
||||
} else {
|
||||
return delegate.fromJson(reader);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
|
||||
if (value == null) {
|
||||
writer.nullValue();
|
||||
} else {
|
||||
delegate.toJson(writer, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
return delegate + ".nullSafe()";
|
||||
}
|
||||
}
|
523
moshi/src/main/java/com/squareup/moshi/internal/Util.java
Normal file
523
moshi/src/main/java/com/squareup/moshi/internal/Util.java
Normal file
|
@ -0,0 +1,523 @@
|
|||
/*
|
||||
* Copyright (C) 2014 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.internal;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.JsonClass;
|
||||
import com.squareup.moshi.JsonQualifier;
|
||||
import com.squareup.moshi.Moshi;
|
||||
import com.squareup.moshi.Types;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.AnnotatedElement;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.GenericArrayType;
|
||||
import java.lang.reflect.GenericDeclaration;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.lang.reflect.TypeVariable;
|
||||
import java.lang.reflect.WildcardType;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static com.squareup.moshi.Types.arrayOf;
|
||||
import static com.squareup.moshi.Types.subtypeOf;
|
||||
import static com.squareup.moshi.Types.supertypeOf;
|
||||
|
||||
public final class Util {
|
||||
public static final Set<Annotation> NO_ANNOTATIONS = Collections.emptySet();
|
||||
public static final Type[] EMPTY_TYPE_ARRAY = new Type[] {};
|
||||
|
||||
private Util() {
|
||||
}
|
||||
|
||||
public static boolean typesMatch(Type pattern, Type candidate) {
|
||||
// TODO: permit raw types (like Set.class) to match non-raw candidates (like Set<Long>).
|
||||
return Types.equals(pattern, candidate);
|
||||
}
|
||||
|
||||
public static Set<? extends Annotation> jsonAnnotations(AnnotatedElement annotatedElement) {
|
||||
return jsonAnnotations(annotatedElement.getAnnotations());
|
||||
}
|
||||
|
||||
public static Set<? extends Annotation> jsonAnnotations(Annotation[] annotations) {
|
||||
Set<Annotation> result = null;
|
||||
for (Annotation annotation : annotations) {
|
||||
if (annotation.annotationType().isAnnotationPresent(JsonQualifier.class)) {
|
||||
if (result == null) result = new LinkedHashSet<>();
|
||||
result.add(annotation);
|
||||
}
|
||||
}
|
||||
return result != null ? Collections.unmodifiableSet(result) : Util.NO_ANNOTATIONS;
|
||||
}
|
||||
|
||||
public static boolean isAnnotationPresent(
|
||||
Set<? extends Annotation> annotations, Class<? extends Annotation> annotationClass) {
|
||||
if (annotations.isEmpty()) return false; // Save an iterator in the common case.
|
||||
for (Annotation annotation : annotations) {
|
||||
if (annotation.annotationType() == annotationClass) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Returns true if {@code annotations} has any annotation whose simple name is Nullable. */
|
||||
public static boolean hasNullable(Annotation[] annotations) {
|
||||
for (Annotation annotation : annotations) {
|
||||
if (annotation.annotationType().getSimpleName().equals("Nullable")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if {@code rawType} is built in. We don't reflect on private fields of platform
|
||||
* types because they're unspecified and likely to be different on Java vs. Android.
|
||||
*/
|
||||
public static boolean isPlatformType(Class<?> rawType) {
|
||||
String name = rawType.getName();
|
||||
return name.startsWith("android.")
|
||||
|| name.startsWith("androidx.")
|
||||
|| name.startsWith("java.")
|
||||
|| name.startsWith("javax.")
|
||||
|| name.startsWith("kotlin.")
|
||||
|| name.startsWith("scala.");
|
||||
}
|
||||
|
||||
/** Throws the cause of {@code e}, wrapping it if it is checked. */
|
||||
public static RuntimeException rethrowCause(InvocationTargetException e) {
|
||||
Throwable cause = e.getTargetException();
|
||||
if (cause instanceof RuntimeException) throw (RuntimeException) cause;
|
||||
if (cause instanceof Error) throw (Error) cause;
|
||||
throw new RuntimeException(cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a type that is functionally equal but not necessarily equal according to {@link
|
||||
* Object#equals(Object) Object.equals()}.
|
||||
*/
|
||||
public static Type canonicalize(Type type) {
|
||||
if (type instanceof Class) {
|
||||
Class<?> c = (Class<?>) type;
|
||||
return c.isArray() ? new GenericArrayTypeImpl(canonicalize(c.getComponentType())) : c;
|
||||
|
||||
} else if (type instanceof ParameterizedType) {
|
||||
if (type instanceof ParameterizedTypeImpl) return type;
|
||||
ParameterizedType p = (ParameterizedType) type;
|
||||
return new ParameterizedTypeImpl(p.getOwnerType(),
|
||||
p.getRawType(), p.getActualTypeArguments());
|
||||
|
||||
} else if (type instanceof GenericArrayType) {
|
||||
if (type instanceof GenericArrayTypeImpl) return type;
|
||||
GenericArrayType g = (GenericArrayType) type;
|
||||
return new GenericArrayTypeImpl(g.getGenericComponentType());
|
||||
|
||||
} else if (type instanceof WildcardType) {
|
||||
if (type instanceof WildcardTypeImpl) return type;
|
||||
WildcardType w = (WildcardType) type;
|
||||
return new WildcardTypeImpl(w.getUpperBounds(), w.getLowerBounds());
|
||||
|
||||
} else {
|
||||
return type; // This type is unsupported!
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If type is a "? extends X" wildcard, returns X; otherwise returns type unchanged.
|
||||
*/
|
||||
public static Type removeSubtypeWildcard(Type type) {
|
||||
if (!(type instanceof WildcardType)) return type;
|
||||
|
||||
Type[] lowerBounds = ((WildcardType) type).getLowerBounds();
|
||||
if (lowerBounds.length != 0) return type;
|
||||
|
||||
Type[] upperBounds = ((WildcardType) type).getUpperBounds();
|
||||
if (upperBounds.length != 1) throw new IllegalArgumentException();
|
||||
|
||||
return upperBounds[0];
|
||||
}
|
||||
|
||||
public static Type resolve(Type context, Class<?> contextRawType, Type toResolve) {
|
||||
// This implementation is made a little more complicated in an attempt to avoid object-creation.
|
||||
while (true) {
|
||||
if (toResolve instanceof TypeVariable) {
|
||||
TypeVariable<?> typeVariable = (TypeVariable<?>) toResolve;
|
||||
toResolve = resolveTypeVariable(context, contextRawType, typeVariable);
|
||||
if (toResolve == typeVariable) return toResolve;
|
||||
|
||||
} else if (toResolve instanceof Class && ((Class<?>) toResolve).isArray()) {
|
||||
Class<?> original = (Class<?>) toResolve;
|
||||
Type componentType = original.getComponentType();
|
||||
Type newComponentType = resolve(context, contextRawType, componentType);
|
||||
return componentType == newComponentType
|
||||
? original
|
||||
: arrayOf(newComponentType);
|
||||
|
||||
} else if (toResolve instanceof GenericArrayType) {
|
||||
GenericArrayType original = (GenericArrayType) toResolve;
|
||||
Type componentType = original.getGenericComponentType();
|
||||
Type newComponentType = resolve(context, contextRawType, componentType);
|
||||
return componentType == newComponentType
|
||||
? original
|
||||
: arrayOf(newComponentType);
|
||||
|
||||
} else if (toResolve instanceof ParameterizedType) {
|
||||
ParameterizedType original = (ParameterizedType) toResolve;
|
||||
Type ownerType = original.getOwnerType();
|
||||
Type newOwnerType = resolve(context, contextRawType, ownerType);
|
||||
boolean changed = newOwnerType != ownerType;
|
||||
|
||||
Type[] args = original.getActualTypeArguments();
|
||||
for (int t = 0, length = args.length; t < length; t++) {
|
||||
Type resolvedTypeArgument = resolve(context, contextRawType, args[t]);
|
||||
if (resolvedTypeArgument != args[t]) {
|
||||
if (!changed) {
|
||||
args = args.clone();
|
||||
changed = true;
|
||||
}
|
||||
args[t] = resolvedTypeArgument;
|
||||
}
|
||||
}
|
||||
|
||||
return changed
|
||||
? new ParameterizedTypeImpl(newOwnerType, original.getRawType(), args)
|
||||
: original;
|
||||
|
||||
} else if (toResolve instanceof WildcardType) {
|
||||
WildcardType original = (WildcardType) toResolve;
|
||||
Type[] originalLowerBound = original.getLowerBounds();
|
||||
Type[] originalUpperBound = original.getUpperBounds();
|
||||
|
||||
if (originalLowerBound.length == 1) {
|
||||
Type lowerBound = resolve(context, contextRawType, originalLowerBound[0]);
|
||||
if (lowerBound != originalLowerBound[0]) {
|
||||
return supertypeOf(lowerBound);
|
||||
}
|
||||
} else if (originalUpperBound.length == 1) {
|
||||
Type upperBound = resolve(context, contextRawType, originalUpperBound[0]);
|
||||
if (upperBound != originalUpperBound[0]) {
|
||||
return subtypeOf(upperBound);
|
||||
}
|
||||
}
|
||||
return original;
|
||||
|
||||
} else {
|
||||
return toResolve;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Type resolveTypeVariable(Type context, Class<?> contextRawType, TypeVariable<?> unknown) {
|
||||
Class<?> declaredByRaw = declaringClassOf(unknown);
|
||||
|
||||
// We can't reduce this further.
|
||||
if (declaredByRaw == null) return unknown;
|
||||
|
||||
Type declaredBy = getGenericSupertype(context, contextRawType, declaredByRaw);
|
||||
if (declaredBy instanceof ParameterizedType) {
|
||||
int index = indexOf(declaredByRaw.getTypeParameters(), unknown);
|
||||
return ((ParameterizedType) declaredBy).getActualTypeArguments()[index];
|
||||
}
|
||||
|
||||
return unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the generic supertype for {@code supertype}. For example, given a class {@code
|
||||
* IntegerSet}, the result for when supertype is {@code Set.class} is {@code Set<Integer>} and the
|
||||
* result when the supertype is {@code Collection.class} is {@code Collection<Integer>}.
|
||||
*/
|
||||
public static Type getGenericSupertype(Type context, Class<?> rawType, Class<?> toResolve) {
|
||||
if (toResolve == rawType) {
|
||||
return context;
|
||||
}
|
||||
|
||||
// we skip searching through interfaces if unknown is an interface
|
||||
if (toResolve.isInterface()) {
|
||||
Class<?>[] interfaces = rawType.getInterfaces();
|
||||
for (int i = 0, length = interfaces.length; i < length; i++) {
|
||||
if (interfaces[i] == toResolve) {
|
||||
return rawType.getGenericInterfaces()[i];
|
||||
} else if (toResolve.isAssignableFrom(interfaces[i])) {
|
||||
return getGenericSupertype(rawType.getGenericInterfaces()[i], interfaces[i], toResolve);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check our supertypes
|
||||
if (!rawType.isInterface()) {
|
||||
while (rawType != Object.class) {
|
||||
Class<?> rawSupertype = rawType.getSuperclass();
|
||||
if (rawSupertype == toResolve) {
|
||||
return rawType.getGenericSuperclass();
|
||||
} else if (toResolve.isAssignableFrom(rawSupertype)) {
|
||||
return getGenericSupertype(rawType.getGenericSuperclass(), rawSupertype, toResolve);
|
||||
}
|
||||
rawType = rawSupertype;
|
||||
}
|
||||
}
|
||||
|
||||
// we can't resolve this further
|
||||
return toResolve;
|
||||
}
|
||||
|
||||
static int hashCodeOrZero(@Nullable Object o) {
|
||||
return o != null ? o.hashCode() : 0;
|
||||
}
|
||||
|
||||
static String typeToString(Type type) {
|
||||
return type instanceof Class ? ((Class<?>) type).getName() : type.toString();
|
||||
}
|
||||
|
||||
static int indexOf(Object[] array, Object toFind) {
|
||||
for (int i = 0; i < array.length; i++) {
|
||||
if (toFind.equals(array[i])) return i;
|
||||
}
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the declaring class of {@code typeVariable}, or {@code null} if it was not declared by
|
||||
* a class.
|
||||
*/
|
||||
static @Nullable Class<?> declaringClassOf(TypeVariable<?> typeVariable) {
|
||||
GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration();
|
||||
return genericDeclaration instanceof Class ? (Class<?>) genericDeclaration : null;
|
||||
}
|
||||
|
||||
static void checkNotPrimitive(Type type) {
|
||||
if ((type instanceof Class<?>) && ((Class<?>) type).isPrimitive()) {
|
||||
throw new IllegalArgumentException("Unexpected primitive " + type + ". Use the boxed type.");
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ParameterizedTypeImpl implements ParameterizedType {
|
||||
private final @Nullable Type ownerType;
|
||||
private final Type rawType;
|
||||
public final Type[] typeArguments;
|
||||
|
||||
public ParameterizedTypeImpl(@Nullable Type ownerType, Type rawType, Type... typeArguments) {
|
||||
// Require an owner type if the raw type needs it.
|
||||
if (rawType instanceof Class<?>) {
|
||||
Class<?> enclosingClass = ((Class<?>) rawType).getEnclosingClass();
|
||||
if (ownerType != null) {
|
||||
if (enclosingClass == null || Types.getRawType(ownerType) != enclosingClass) {
|
||||
throw new IllegalArgumentException(
|
||||
"unexpected owner type for " + rawType + ": " + ownerType);
|
||||
}
|
||||
} else if (enclosingClass != null) {
|
||||
throw new IllegalArgumentException(
|
||||
"unexpected owner type for " + rawType + ": null");
|
||||
}
|
||||
}
|
||||
|
||||
this.ownerType = ownerType == null ? null : canonicalize(ownerType);
|
||||
this.rawType = canonicalize(rawType);
|
||||
this.typeArguments = typeArguments.clone();
|
||||
for (int t = 0; t < this.typeArguments.length; t++) {
|
||||
if (this.typeArguments[t] == null) throw new NullPointerException();
|
||||
checkNotPrimitive(this.typeArguments[t]);
|
||||
this.typeArguments[t] = canonicalize(this.typeArguments[t]);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public Type[] getActualTypeArguments() {
|
||||
return typeArguments.clone();
|
||||
}
|
||||
|
||||
@Override public Type getRawType() {
|
||||
return rawType;
|
||||
}
|
||||
|
||||
@Override public @Nullable Type getOwnerType() {
|
||||
return ownerType;
|
||||
}
|
||||
|
||||
@Override public boolean equals(Object other) {
|
||||
return other instanceof ParameterizedType
|
||||
&& Types.equals(this, (ParameterizedType) other);
|
||||
}
|
||||
|
||||
@Override public int hashCode() {
|
||||
return Arrays.hashCode(typeArguments)
|
||||
^ rawType.hashCode()
|
||||
^ hashCodeOrZero(ownerType);
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
StringBuilder result = new StringBuilder(30 * (typeArguments.length + 1));
|
||||
result.append(typeToString(rawType));
|
||||
|
||||
if (typeArguments.length == 0) {
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
result.append("<").append(typeToString(typeArguments[0]));
|
||||
for (int i = 1; i < typeArguments.length; i++) {
|
||||
result.append(", ").append(typeToString(typeArguments[i]));
|
||||
}
|
||||
return result.append(">").toString();
|
||||
}
|
||||
}
|
||||
|
||||
public static final class GenericArrayTypeImpl implements GenericArrayType {
|
||||
private final Type componentType;
|
||||
|
||||
public GenericArrayTypeImpl(Type componentType) {
|
||||
this.componentType = canonicalize(componentType);
|
||||
}
|
||||
|
||||
@Override public Type getGenericComponentType() {
|
||||
return componentType;
|
||||
}
|
||||
|
||||
@Override public boolean equals(Object o) {
|
||||
return o instanceof GenericArrayType
|
||||
&& Types.equals(this, (GenericArrayType) o);
|
||||
}
|
||||
|
||||
@Override public int hashCode() {
|
||||
return componentType.hashCode();
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
return typeToString(componentType) + "[]";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The WildcardType interface supports multiple upper bounds and multiple lower bounds. We only
|
||||
* support what the Java 6 language needs - at most one bound. If a lower bound is set, the upper
|
||||
* bound must be Object.class.
|
||||
*/
|
||||
public static final class WildcardTypeImpl implements WildcardType {
|
||||
private final Type upperBound;
|
||||
private final @Nullable Type lowerBound;
|
||||
|
||||
public WildcardTypeImpl(Type[] upperBounds, Type[] lowerBounds) {
|
||||
if (lowerBounds.length > 1) throw new IllegalArgumentException();
|
||||
if (upperBounds.length != 1) throw new IllegalArgumentException();
|
||||
|
||||
if (lowerBounds.length == 1) {
|
||||
if (lowerBounds[0] == null) throw new NullPointerException();
|
||||
checkNotPrimitive(lowerBounds[0]);
|
||||
if (upperBounds[0] != Object.class) throw new IllegalArgumentException();
|
||||
this.lowerBound = canonicalize(lowerBounds[0]);
|
||||
this.upperBound = Object.class;
|
||||
|
||||
} else {
|
||||
if (upperBounds[0] == null) throw new NullPointerException();
|
||||
checkNotPrimitive(upperBounds[0]);
|
||||
this.lowerBound = null;
|
||||
this.upperBound = canonicalize(upperBounds[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public Type[] getUpperBounds() {
|
||||
return new Type[] { upperBound };
|
||||
}
|
||||
|
||||
@Override public Type[] getLowerBounds() {
|
||||
return lowerBound != null ? new Type[] { lowerBound } : EMPTY_TYPE_ARRAY;
|
||||
}
|
||||
|
||||
@Override public boolean equals(Object other) {
|
||||
return other instanceof WildcardType
|
||||
&& Types.equals(this, (WildcardType) other);
|
||||
}
|
||||
|
||||
@Override public int hashCode() {
|
||||
// This equals Arrays.hashCode(getLowerBounds()) ^ Arrays.hashCode(getUpperBounds()).
|
||||
return (lowerBound != null ? 31 + lowerBound.hashCode() : 1)
|
||||
^ (31 + upperBound.hashCode());
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
if (lowerBound != null) {
|
||||
return "? super " + typeToString(lowerBound);
|
||||
} else if (upperBound == Object.class) {
|
||||
return "?";
|
||||
} else {
|
||||
return "? extends " + typeToString(upperBound);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static String typeAnnotatedWithAnnotations(Type type,
|
||||
Set<? extends Annotation> annotations) {
|
||||
return type + (annotations.isEmpty() ? " (with no annotations)" : " annotated " + annotations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the generated JsonAdapter for classes annotated {@link JsonClass}. This works because it
|
||||
* uses the same naming conventions as {@code JsonClassCodeGenProcessor}.
|
||||
*/
|
||||
public static @Nullable JsonAdapter<?> generatedAdapter(Moshi moshi, Type type,
|
||||
Class<?> rawType) {
|
||||
JsonClass jsonClass = rawType.getAnnotation(JsonClass.class);
|
||||
if (jsonClass == null || !jsonClass.generateAdapter()) {
|
||||
return null;
|
||||
}
|
||||
String adapterClassName = Types.generatedJsonAdapterName(rawType.getName());
|
||||
try {
|
||||
@SuppressWarnings("unchecked") // We generate types to match.
|
||||
Class<? extends JsonAdapter<?>> adapterClass = (Class<? extends JsonAdapter<?>>)
|
||||
Class.forName(adapterClassName, true, rawType.getClassLoader());
|
||||
Constructor<? extends JsonAdapter<?>> constructor;
|
||||
Object[] args;
|
||||
if (type instanceof ParameterizedType) {
|
||||
Type[] typeArgs = ((ParameterizedType) type).getActualTypeArguments();
|
||||
try {
|
||||
// Common case first
|
||||
constructor = adapterClass.getDeclaredConstructor(Moshi.class, Type[].class);
|
||||
args = new Object[] { moshi, typeArgs };
|
||||
} catch (NoSuchMethodException e) {
|
||||
constructor = adapterClass.getDeclaredConstructor(Type[].class);
|
||||
args = new Object[] { typeArgs };
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
// Common case first
|
||||
constructor = adapterClass.getDeclaredConstructor(Moshi.class);
|
||||
args = new Object[] { moshi };
|
||||
} catch (NoSuchMethodException e) {
|
||||
constructor = adapterClass.getDeclaredConstructor();
|
||||
args = new Object[0];
|
||||
}
|
||||
}
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance(args).nullSafe();
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException(
|
||||
"Failed to find the generated JsonAdapter class for " + rawType, e);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new RuntimeException(
|
||||
"Failed to find the generated JsonAdapter constructor for " + rawType, e);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(
|
||||
"Failed to access the generated JsonAdapter for " + rawType, e);
|
||||
} catch (InstantiationException e) {
|
||||
throw new RuntimeException(
|
||||
"Failed to instantiate the generated JsonAdapter for " + rawType, e);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw rethrowCause(e);
|
||||
}
|
||||
}
|
||||
}
|
3
moshi/src/main/java/com/squareup/moshi/package-info.java
Normal file
3
moshi/src/main/java/com/squareup/moshi/package-info.java
Normal file
|
@ -0,0 +1,3 @@
|
|||
/** Moshi is modern JSON library for Android and Java. */
|
||||
@javax.annotation.ParametersAreNonnullByDefault
|
||||
package com.squareup.moshi;
|
49
moshi/src/main/resources/META-INF/proguard/moshi.pro
Normal file
49
moshi/src/main/resources/META-INF/proguard/moshi.pro
Normal file
|
@ -0,0 +1,49 @@
|
|||
# JSR 305 annotations are for embedding nullability information.
|
||||
-dontwarn javax.annotation.**
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@com.squareup.moshi.* <methods>;
|
||||
}
|
||||
|
||||
-keep @com.squareup.moshi.JsonQualifier interface *
|
||||
|
||||
# Enum field names are used by the integrated EnumJsonAdapter.
|
||||
# Annotate enums with @JsonClass(generateAdapter = false) to use them with Moshi.
|
||||
-keepclassmembers @com.squareup.moshi.JsonClass class * extends java.lang.Enum {
|
||||
<fields>;
|
||||
}
|
||||
|
||||
# The name of @JsonClass types is used to look up the generated adapter.
|
||||
-keepnames @com.squareup.moshi.JsonClass class *
|
||||
|
||||
# Retain generated JsonAdapters if annotated type is retained.
|
||||
-if @com.squareup.moshi.JsonClass class *
|
||||
-keep class <1>JsonAdapter {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
||||
-if @com.squareup.moshi.JsonClass class **$*
|
||||
-keep class <1>_<2>JsonAdapter {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
||||
-if @com.squareup.moshi.JsonClass class **$*$*
|
||||
-keep class <1>_<2>_<3>JsonAdapter {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
||||
-if @com.squareup.moshi.JsonClass class **$*$*$*
|
||||
-keep class <1>_<2>_<3>_<4>JsonAdapter {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
||||
-if @com.squareup.moshi.JsonClass class **$*$*$*$*
|
||||
-keep class <1>_<2>_<3>_<4>_<5>JsonAdapter {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
||||
-if @com.squareup.moshi.JsonClass class **$*$*$*$*$*
|
||||
-keep class <1>_<2>_<3>_<4>_<5>_<6>JsonAdapter {
|
||||
<init>(...);
|
||||
<fields>;
|
||||
}
|
|
@ -15,6 +15,8 @@
|
|||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import com.squareup.moshi.MoshiTest.Uppercase;
|
||||
import com.squareup.moshi.MoshiTest.UppercaseAdapterFactory;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
@ -24,8 +26,10 @@ import java.util.ArrayList;
|
|||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import okio.ByteString;
|
||||
import org.junit.Test;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
@ -79,6 +83,91 @@ public final class AdapterMethodsTest {
|
|||
}
|
||||
}
|
||||
|
||||
private static final class PointJsonAdapterWithDelegate {
|
||||
@FromJson Point fromJson(JsonReader reader, JsonAdapter<Point> delegate) throws IOException {
|
||||
reader.beginArray();
|
||||
Point value = delegate.fromJson(reader);
|
||||
reader.endArray();
|
||||
return value;
|
||||
}
|
||||
|
||||
@ToJson void toJson(JsonWriter writer, Point value, JsonAdapter<Point> delegate)
|
||||
throws IOException {
|
||||
writer.beginArray();
|
||||
delegate.toJson(writer, value);
|
||||
writer.endArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PointJsonAdapterWithDelegateWithQualifier {
|
||||
@FromJson @WithParens Point fromJson(JsonReader reader, @WithParens JsonAdapter<Point> delegate)
|
||||
throws IOException {
|
||||
reader.beginArray();
|
||||
Point value = delegate.fromJson(reader);
|
||||
reader.endArray();
|
||||
return value;
|
||||
}
|
||||
|
||||
@ToJson void toJson(JsonWriter writer, @WithParens Point value,
|
||||
@WithParens JsonAdapter<Point> delegate)
|
||||
throws IOException {
|
||||
writer.beginArray();
|
||||
delegate.toJson(writer, value);
|
||||
writer.endArray();
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void toAndFromWithDelegate() throws Exception {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(new PointJsonAdapterWithDelegate())
|
||||
.build();
|
||||
JsonAdapter<Point> adapter = moshi.adapter(Point.class);
|
||||
Point point = new Point(5, 8);
|
||||
assertThat(adapter.toJson(point)).isEqualTo("[{\"x\":5,\"y\":8}]");
|
||||
assertThat(adapter.fromJson("[{\"x\":5,\"y\":8}]")).isEqualTo(point);
|
||||
}
|
||||
|
||||
@Test public void toAndFromWithDelegateWithQualifier() throws Exception {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(new PointJsonAdapterWithDelegateWithQualifier())
|
||||
.add(new PointWithParensJsonAdapter())
|
||||
.build();
|
||||
JsonAdapter<Point> adapter = moshi.adapter(Point.class, WithParens.class);
|
||||
Point point = new Point(5, 8);
|
||||
assertThat(adapter.toJson(point)).isEqualTo("[\"(5 8)\"]");
|
||||
assertThat(adapter.fromJson("[\"(5 8)\"]")).isEqualTo(point);
|
||||
}
|
||||
|
||||
@Test public void toAndFromWithIntermediate() throws Exception {
|
||||
Moshi moshi = new Moshi.Builder().add(new Object() {
|
||||
@FromJson String fromJson(String string) {
|
||||
return string.substring(1, string.length() - 1);
|
||||
}
|
||||
|
||||
@ToJson String toJson(String value) {
|
||||
return "|" + value + "|";
|
||||
}
|
||||
}).build();
|
||||
JsonAdapter<String> adapter = moshi.adapter(String.class);
|
||||
assertThat(adapter.toJson("pizza")).isEqualTo("\"|pizza|\"");
|
||||
assertThat(adapter.fromJson("\"|pizza|\"")).isEqualTo("pizza");
|
||||
}
|
||||
|
||||
@Test public void toAndFromWithIntermediateWithQualifier() throws Exception {
|
||||
Moshi moshi = new Moshi.Builder().add(new Object() {
|
||||
@FromJson @Uppercase String fromJson(@Uppercase String string) {
|
||||
return string.substring(1, string.length() - 1);
|
||||
}
|
||||
|
||||
@ToJson @Uppercase String toJson(@Uppercase String value) {
|
||||
return "|" + value + "|";
|
||||
}
|
||||
}).add(new UppercaseAdapterFactory()).build();
|
||||
JsonAdapter<String> adapter = moshi.adapter(String.class, Uppercase.class);
|
||||
assertThat(adapter.toJson("pizza")).isEqualTo("\"|PIZZA|\"");
|
||||
assertThat(adapter.fromJson("\"|pizza|\"")).isEqualTo("PIZZA");
|
||||
}
|
||||
|
||||
@Test public void toJsonOnly() throws Exception {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(new PointAsListOfIntegersToAdapter())
|
||||
|
@ -215,6 +304,8 @@ public final class AdapterMethodsTest {
|
|||
+ "(com.squareup.moshi.AdapterMethodsTest$Point).\n"
|
||||
+ "@ToJson method signatures may have one of the following structures:\n"
|
||||
+ " <any access modifier> void toJson(JsonWriter writer, T value) throws <any>;\n"
|
||||
+ " <any access modifier> void toJson(JsonWriter writer, T value,"
|
||||
+ " JsonAdapter<any> delegate, <any more delegates>) throws <any>;\n"
|
||||
+ " <any access modifier> R toJson(T value) throws <any>;\n");
|
||||
}
|
||||
}
|
||||
|
@ -234,7 +325,9 @@ public final class AdapterMethodsTest {
|
|||
+ "com.squareup.moshi.AdapterMethodsTest$UnexpectedSignatureFromJsonAdapter.pointFromJson"
|
||||
+ "(java.lang.String).\n"
|
||||
+ "@FromJson method signatures may have one of the following structures:\n"
|
||||
+ " <any access modifier> void fromJson(JsonReader jsonReader) throws <any>;\n"
|
||||
+ " <any access modifier> R fromJson(JsonReader jsonReader) throws <any>;\n"
|
||||
+ " <any access modifier> R fromJson(JsonReader jsonReader,"
|
||||
+ " JsonAdapter<any> delegate, <any more delegates>) throws <any>;\n"
|
||||
+ " <any access modifier> R fromJson(T value) throws <any>;\n");
|
||||
}
|
||||
}
|
||||
|
@ -304,7 +397,7 @@ public final class AdapterMethodsTest {
|
|||
}
|
||||
|
||||
static class NullableIntToJsonAdapter {
|
||||
@FromJson int intToJson(JsonReader reader) throws IOException {
|
||||
@FromJson int jsonToInt(JsonReader reader) throws IOException {
|
||||
if (reader.peek() == JsonReader.Token.NULL) {
|
||||
reader.nextNull();
|
||||
return -1;
|
||||
|
@ -312,7 +405,7 @@ public final class AdapterMethodsTest {
|
|||
return reader.nextInt();
|
||||
}
|
||||
|
||||
@ToJson void jsonToInt(JsonWriter writer, int value) throws IOException {
|
||||
@ToJson void intToJson(JsonWriter writer, int value) throws IOException {
|
||||
if (value == -1) {
|
||||
writer.nullValue();
|
||||
} else {
|
||||
|
@ -369,7 +462,10 @@ public final class AdapterMethodsTest {
|
|||
fail();
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertThat(e).hasMessage("No @FromJson adapter for interface "
|
||||
+ "com.squareup.moshi.AdapterMethodsTest$Shape annotated []");
|
||||
+ "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)");
|
||||
assertThat(e).hasCauseExactlyInstanceOf(IllegalArgumentException.class);
|
||||
assertThat(e.getCause()).hasMessage("No next JsonAdapter for interface "
|
||||
+ "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -388,7 +484,10 @@ public final class AdapterMethodsTest {
|
|||
fail();
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertThat(e).hasMessage("No @ToJson adapter for interface "
|
||||
+ "com.squareup.moshi.AdapterMethodsTest$Shape annotated []");
|
||||
+ "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)");
|
||||
assertThat(e).hasCauseExactlyInstanceOf(IllegalArgumentException.class);
|
||||
assertThat(e.getCause()).hasMessage("No next JsonAdapter for interface "
|
||||
+ "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -615,6 +714,57 @@ public final class AdapterMethodsTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test public void genericArrayTypes() throws Exception {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(new ByteArrayJsonAdapter())
|
||||
.build();
|
||||
JsonAdapter<MapOfByteArrays> jsonAdapter = moshi.adapter(MapOfByteArrays.class);
|
||||
|
||||
MapOfByteArrays mapOfByteArrays = new MapOfByteArrays(
|
||||
Collections.singletonMap("a", new byte[] { 0, -1}));
|
||||
String json = "{\"map\":{\"a\":\"00ff\"}}";
|
||||
|
||||
assertThat(jsonAdapter.toJson(mapOfByteArrays)).isEqualTo(json);
|
||||
assertThat(jsonAdapter.fromJson(json)).isEqualTo(mapOfByteArrays);
|
||||
}
|
||||
|
||||
static class ByteArrayJsonAdapter {
|
||||
@ToJson String byteArrayToJson(byte[] b) {
|
||||
return ByteString.of(b).hex();
|
||||
}
|
||||
|
||||
@FromJson byte[] byteArrayFromJson(String s) throws Exception {
|
||||
return ByteString.decodeHex(s).toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
static class MapOfByteArrays {
|
||||
final Map<String, byte[]> map;
|
||||
|
||||
public MapOfByteArrays(Map<String, byte[]> map) {
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
@Override public boolean equals(Object o) {
|
||||
return o instanceof MapOfByteArrays && o.toString().equals(toString());
|
||||
}
|
||||
|
||||
@Override public int hashCode() {
|
||||
return toString().hashCode();
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (Map.Entry<String, byte[]> entry : map.entrySet()) {
|
||||
if (result.length() > 0) result.append(", ");
|
||||
result.append(entry.getKey())
|
||||
.append(":")
|
||||
.append(Arrays.toString(entry.getValue()));
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
|
||||
static class Point {
|
||||
final int x;
|
||||
final int y;
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import com.squareup.moshi.internal.Util;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.Retention;
|
||||
|
|
|
@ -25,7 +25,7 @@ import okio.Buffer;
|
|||
import org.junit.Test;
|
||||
|
||||
import static com.squareup.moshi.TestUtil.newReader;
|
||||
import static com.squareup.moshi.Util.NO_ANNOTATIONS;
|
||||
import static com.squareup.moshi.internal.Util.NO_ANNOTATIONS;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
|
@ -341,8 +341,23 @@ public final class ClassJsonAdapterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test public void localClassNotSupported() throws Exception {
|
||||
class Local {
|
||||
}
|
||||
try {
|
||||
ClassJsonAdapter.FACTORY.create(Local.class, NO_ANNOTATIONS, moshi);
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
assertThat(expected).hasMessage("Cannot serialize local class "
|
||||
+ "com.squareup.moshi.ClassJsonAdapterTest$1Local");
|
||||
}
|
||||
}
|
||||
|
||||
interface Interface {
|
||||
}
|
||||
|
||||
@Test public void interfaceNotSupported() throws Exception {
|
||||
assertThat(ClassJsonAdapter.FACTORY.create(Runnable.class, NO_ANNOTATIONS, moshi)).isNull();
|
||||
assertThat(ClassJsonAdapter.FACTORY.create(Interface.class, NO_ANNOTATIONS, moshi)).isNull();
|
||||
}
|
||||
|
||||
static abstract class Abstract {
|
||||
|
@ -429,6 +444,23 @@ public final class ClassJsonAdapterTest {
|
|||
assertThat(fromJson.zipCode).isEqualTo("94043");
|
||||
}
|
||||
|
||||
static final class Box<T> {
|
||||
final T data;
|
||||
|
||||
Box(T data) {
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void parameterizedType() throws Exception {
|
||||
@SuppressWarnings("unchecked")
|
||||
JsonAdapter<Box<Integer>> adapter = (JsonAdapter<Box<Integer>>) ClassJsonAdapter.FACTORY.create(
|
||||
Types.newParameterizedTypeWithOwner(ClassJsonAdapterTest.class, Box.class, Integer.class),
|
||||
NO_ANNOTATIONS, moshi);
|
||||
assertThat(adapter.fromJson("{\"data\":5}").data).isEqualTo(5);
|
||||
assertThat(adapter.toJson(new Box<>(5))).isEqualTo("{\"data\":5}");
|
||||
}
|
||||
|
||||
private <T> String toJson(Class<T> type, T value) throws IOException {
|
||||
@SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument.
|
||||
JsonAdapter<T> jsonAdapter = (JsonAdapter<T>) ClassJsonAdapter.FACTORY.create(
|
||||
|
|
120
moshi/src/test/java/com/squareup/moshi/DeferredAdapterTest.java
Normal file
120
moshi/src/test/java/com/squareup/moshi/DeferredAdapterTest.java
Normal file
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import javax.annotation.Nullable;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public final class DeferredAdapterTest {
|
||||
/**
|
||||
* When a type's JsonAdapter is circularly-dependent, Moshi creates a 'deferred adapter' to make
|
||||
* the cycle work. It's important that any adapters that depend on this deferred adapter don't
|
||||
* leak out until it's ready.
|
||||
*
|
||||
* <p>This test sets up a circular dependency [BlueNode -> GreenNode -> BlueNode] and then tries
|
||||
* to use a GreenNode JSON adapter before the BlueNode JSON adapter is built. It creates a
|
||||
* similar cycle [BlueNode -> RedNode -> BlueNode] so the order adapters are retrieved is
|
||||
* insignificant.
|
||||
*
|
||||
* <p>This used to trigger a crash because we'd incorrectly put the GreenNode JSON adapter in the
|
||||
* cache even though it depended upon an incomplete BlueNode JSON adapter.
|
||||
*/
|
||||
@Test public void concurrentSafe() {
|
||||
final List<Throwable> failures = new ArrayList<>();
|
||||
|
||||
JsonAdapter.Factory factory = new JsonAdapter.Factory() {
|
||||
int redAndGreenCount = 0;
|
||||
|
||||
@Override public @Nullable JsonAdapter<?> create(
|
||||
Type type, Set<? extends Annotation> annotations, final Moshi moshi) {
|
||||
if ((type == RedNode.class || type == GreenNode.class) && redAndGreenCount++ == 1) {
|
||||
doInAnotherThread(new Runnable() {
|
||||
@Override public void run() {
|
||||
GreenNode greenBlue = new GreenNode(new BlueNode(null, null));
|
||||
assertThat(moshi.adapter(GreenNode.class).toJson(greenBlue))
|
||||
.isEqualTo("{\"blue\":{}}");
|
||||
|
||||
RedNode redBlue = new RedNode(new BlueNode(null, null));
|
||||
assertThat(moshi.adapter(RedNode.class).toJson(redBlue))
|
||||
.isEqualTo("{\"blue\":{}}");
|
||||
}
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(factory)
|
||||
.build();
|
||||
|
||||
JsonAdapter<BlueNode> jsonAdapter = moshi.adapter(BlueNode.class);
|
||||
assertThat(jsonAdapter.toJson(new BlueNode(new GreenNode(new BlueNode(null, null)), null)))
|
||||
.isEqualTo("{\"green\":{\"blue\":{}}}");
|
||||
|
||||
assertThat(failures).isEmpty();
|
||||
}
|
||||
|
||||
private void doInAnotherThread(Runnable runnable) {
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
Future<?> future = executor.submit(runnable);
|
||||
executor.shutdown();
|
||||
try {
|
||||
future.get();
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (ExecutionException e) {
|
||||
throw new RuntimeException(e.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
static class BlueNode {
|
||||
@Nullable GreenNode green;
|
||||
@Nullable RedNode red;
|
||||
|
||||
BlueNode(@Nullable GreenNode green, @Nullable RedNode red) {
|
||||
this.green = green;
|
||||
this.red = red;
|
||||
}
|
||||
}
|
||||
|
||||
static class RedNode {
|
||||
@Nullable BlueNode blue;
|
||||
|
||||
RedNode(@Nullable BlueNode blue) {
|
||||
this.blue = blue;
|
||||
}
|
||||
}
|
||||
|
||||
static class GreenNode {
|
||||
@Nullable BlueNode blue;
|
||||
|
||||
GreenNode(@Nullable BlueNode blue) {
|
||||
this.blue = blue;
|
||||
}
|
||||
}
|
||||
}
|
338
moshi/src/test/java/com/squareup/moshi/FlattenTest.java
Normal file
338
moshi/src/test/java/com/squareup/moshi/FlattenTest.java
Normal file
|
@ -0,0 +1,338 @@
|
|||
/*
|
||||
* Copyright (C) 2018 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
import org.junit.runners.Parameterized.Parameter;
|
||||
import org.junit.runners.Parameterized.Parameters;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
/** Note that this test makes heavy use of nested blocks, but these are for readability only. */
|
||||
@RunWith(Parameterized.class)
|
||||
public final class FlattenTest {
|
||||
@Parameter public JsonCodecFactory factory;
|
||||
|
||||
@Parameters(name = "{0}")
|
||||
public static List<Object[]> parameters() {
|
||||
return JsonCodecFactory.factories();
|
||||
}
|
||||
|
||||
@Test public void flattenExample() throws Exception {
|
||||
Moshi moshi = new Moshi.Builder().build();
|
||||
JsonAdapter<List<Integer>> integersAdapter =
|
||||
moshi.adapter(Types.newParameterizedType(List.class, Integer.class));
|
||||
|
||||
JsonWriter writer = factory.newWriter();
|
||||
writer.beginArray();
|
||||
int token = writer.beginFlatten();
|
||||
writer.value(1);
|
||||
integersAdapter.toJson(writer, Arrays.asList(2, 3, 4));
|
||||
writer.value(5);
|
||||
writer.endFlatten(token);
|
||||
writer.endArray();
|
||||
|
||||
assertThat(factory.json()).isEqualTo("[1,2,3,4,5]");
|
||||
}
|
||||
|
||||
@Test public void flattenObject() throws Exception {
|
||||
JsonWriter writer = factory.newWriter();
|
||||
writer.beginObject();
|
||||
{
|
||||
writer.name("a");
|
||||
writer.value("aaa");
|
||||
int token = writer.beginFlatten();
|
||||
{
|
||||
writer.beginObject();
|
||||
{
|
||||
writer.name("b");
|
||||
writer.value("bbb");
|
||||
}
|
||||
writer.endObject();
|
||||
}
|
||||
writer.endFlatten(token);
|
||||
writer.name("c");
|
||||
writer.value("ccc");
|
||||
}
|
||||
writer.endObject();
|
||||
assertThat(factory.json()).isEqualTo("{\"a\":\"aaa\",\"b\":\"bbb\",\"c\":\"ccc\"}");
|
||||
}
|
||||
|
||||
@Test public void flattenArray() throws Exception {
|
||||
JsonWriter writer = factory.newWriter();
|
||||
writer.beginArray();
|
||||
{
|
||||
writer.value("a");
|
||||
int token = writer.beginFlatten();
|
||||
{
|
||||
writer.beginArray();
|
||||
{
|
||||
writer.value("b");
|
||||
}
|
||||
writer.endArray();
|
||||
}
|
||||
writer.endFlatten(token);
|
||||
writer.value("c");
|
||||
}
|
||||
writer.endArray();
|
||||
assertThat(factory.json()).isEqualTo("[\"a\",\"b\",\"c\"]");
|
||||
}
|
||||
|
||||
@Test public void recursiveFlatten() throws Exception {
|
||||
JsonWriter writer = factory.newWriter();
|
||||
writer.beginArray();
|
||||
{
|
||||
writer.value("a");
|
||||
int token1 = writer.beginFlatten();
|
||||
{
|
||||
writer.beginArray();
|
||||
{
|
||||
writer.value("b");
|
||||
int token2 = writer.beginFlatten();
|
||||
{
|
||||
writer.beginArray();
|
||||
{
|
||||
writer.value("c");
|
||||
}
|
||||
writer.endArray();
|
||||
}
|
||||
writer.endFlatten(token2);
|
||||
writer.value("d");
|
||||
}
|
||||
writer.endArray();
|
||||
}
|
||||
writer.endFlatten(token1);
|
||||
writer.value("e");
|
||||
}
|
||||
writer.endArray();
|
||||
assertThat(factory.json()).isEqualTo("[\"a\",\"b\",\"c\",\"d\",\"e\"]");
|
||||
}
|
||||
|
||||
@Test public void flattenMultipleNested() throws Exception {
|
||||
JsonWriter writer = factory.newWriter();
|
||||
writer.beginArray();
|
||||
{
|
||||
writer.value("a");
|
||||
int token = writer.beginFlatten();
|
||||
{
|
||||
writer.beginArray();
|
||||
{
|
||||
writer.value("b");
|
||||
}
|
||||
writer.endArray();
|
||||
writer.beginArray();
|
||||
{
|
||||
writer.value("c");
|
||||
}
|
||||
writer.endArray();
|
||||
}
|
||||
writer.endFlatten(token);
|
||||
writer.value("d");
|
||||
}
|
||||
writer.endArray();
|
||||
assertThat(factory.json()).isEqualTo("[\"a\",\"b\",\"c\",\"d\"]");
|
||||
}
|
||||
|
||||
@Test public void flattenIsOnlyOneLevelDeep() throws Exception {
|
||||
JsonWriter writer = factory.newWriter();
|
||||
writer.beginArray();
|
||||
{
|
||||
writer.value("a");
|
||||
int token = writer.beginFlatten();
|
||||
{
|
||||
writer.beginArray();
|
||||
{
|
||||
writer.value("b");
|
||||
writer.beginArray();
|
||||
{
|
||||
writer.value("c");
|
||||
}
|
||||
writer.endArray();
|
||||
writer.value("d");
|
||||
}
|
||||
writer.endArray();
|
||||
}
|
||||
writer.endFlatten(token);
|
||||
writer.value("e");
|
||||
}
|
||||
writer.endArray();
|
||||
assertThat(factory.json()).isEqualTo("[\"a\",\"b\",[\"c\"],\"d\",\"e\"]");
|
||||
}
|
||||
|
||||
@Test public void flattenOnlySomeChildren() throws Exception {
|
||||
JsonWriter writer = factory.newWriter();
|
||||
writer.beginArray();
|
||||
{
|
||||
writer.value("a");
|
||||
int token = writer.beginFlatten();
|
||||
{
|
||||
writer.beginArray();
|
||||
{
|
||||
writer.value("b");
|
||||
}
|
||||
writer.endArray();
|
||||
}
|
||||
writer.endFlatten(token);
|
||||
writer.beginArray();
|
||||
{
|
||||
writer.value("c");
|
||||
}
|
||||
writer.endArray();
|
||||
writer.value("d");
|
||||
}
|
||||
writer.endArray();
|
||||
assertThat(factory.json()).isEqualTo("[\"a\",\"b\",[\"c\"],\"d\"]");
|
||||
}
|
||||
|
||||
@Test public void multipleCallsToFlattenSameNesting() throws Exception {
|
||||
JsonWriter writer = factory.newWriter();
|
||||
writer.beginArray();
|
||||
{
|
||||
writer.value("a");
|
||||
int token1 = writer.beginFlatten();
|
||||
{
|
||||
writer.beginArray();
|
||||
{
|
||||
writer.value("b");
|
||||
}
|
||||
writer.endArray();
|
||||
int token2 = writer.beginFlatten();
|
||||
{
|
||||
writer.beginArray();
|
||||
{
|
||||
writer.value("c");
|
||||
}
|
||||
writer.endArray();
|
||||
}
|
||||
writer.endFlatten(token2);
|
||||
writer.beginArray();
|
||||
{
|
||||
writer.value("d");
|
||||
}
|
||||
writer.endArray();
|
||||
}
|
||||
writer.endFlatten(token1);
|
||||
writer.value("e");
|
||||
}
|
||||
writer.endArray();
|
||||
assertThat(factory.json()).isEqualTo("[\"a\",\"b\",\"c\",\"d\",\"e\"]");
|
||||
}
|
||||
|
||||
@Test public void deepFlatten() throws Exception {
|
||||
JsonWriter writer = factory.newWriter();
|
||||
writer.beginArray();
|
||||
{
|
||||
int token1 = writer.beginFlatten();
|
||||
{
|
||||
writer.beginArray();
|
||||
{
|
||||
int token2 = writer.beginFlatten();
|
||||
{
|
||||
writer.beginArray();
|
||||
{
|
||||
int token3 = writer.beginFlatten();
|
||||
{
|
||||
writer.beginArray();
|
||||
{
|
||||
writer.value("a");
|
||||
}
|
||||
writer.endArray();
|
||||
}
|
||||
writer.endFlatten(token3);
|
||||
}
|
||||
writer.endArray();
|
||||
}
|
||||
writer.endFlatten(token2);
|
||||
}
|
||||
writer.endArray();
|
||||
}
|
||||
writer.endFlatten(token1);
|
||||
}
|
||||
writer.endArray();
|
||||
assertThat(factory.json()).isEqualTo("[\"a\"]");
|
||||
}
|
||||
|
||||
@Test public void flattenTopLevel() {
|
||||
JsonWriter writer = factory.newWriter();
|
||||
try {
|
||||
writer.beginFlatten();
|
||||
fail();
|
||||
} catch (IllegalStateException e) {
|
||||
assertThat(e).hasMessage("Nesting problem.");
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void flattenDoesNotImpactOtherTypesInObjects() throws Exception {
|
||||
JsonWriter writer = factory.newWriter();
|
||||
writer.beginObject();
|
||||
{
|
||||
int token = writer.beginFlatten();
|
||||
writer.name("a");
|
||||
writer.beginArray();
|
||||
writer.value("aaa");
|
||||
writer.endArray();
|
||||
writer.beginObject();
|
||||
{
|
||||
writer.name("b");
|
||||
writer.value("bbb");
|
||||
}
|
||||
writer.endObject();
|
||||
writer.name("c");
|
||||
writer.beginArray();
|
||||
writer.value("ccc");
|
||||
writer.endArray();
|
||||
writer.endFlatten(token);
|
||||
}
|
||||
writer.endObject();
|
||||
assertThat(factory.json()).isEqualTo("{\"a\":[\"aaa\"],\"b\":\"bbb\",\"c\":[\"ccc\"]}");
|
||||
}
|
||||
|
||||
@Test public void flattenDoesNotImpactOtherTypesInArrays() throws Exception {
|
||||
JsonWriter writer = factory.newWriter();
|
||||
writer.beginArray();
|
||||
{
|
||||
int token = writer.beginFlatten();
|
||||
{
|
||||
writer.beginObject();
|
||||
{
|
||||
writer.name("a");
|
||||
writer.value("aaa");
|
||||
}
|
||||
writer.endObject();
|
||||
writer.beginArray();
|
||||
{
|
||||
writer.value("bbb");
|
||||
}
|
||||
writer.endArray();
|
||||
writer.value("ccc");
|
||||
writer.beginObject();
|
||||
{
|
||||
writer.name("d");
|
||||
writer.value("ddd");
|
||||
}
|
||||
writer.endObject();
|
||||
}
|
||||
writer.endFlatten(token);
|
||||
}
|
||||
writer.endArray();
|
||||
assertThat(factory.json()).isEqualTo("[{\"a\":\"aaa\"},\"bbb\",\"ccc\",{\"d\":\"ddd\"}]");
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ import java.util.Collections;
|
|||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
|
@ -94,6 +95,45 @@ public final class JsonAdapterTest {
|
|||
assertThat(factory.json()).isEqualTo("[\"A\",null,\"C\"]");
|
||||
}
|
||||
|
||||
@Test public void nonNull() throws Exception {
|
||||
JsonAdapter<String> toUpperCase = new JsonAdapter<String>() {
|
||||
@Override public String fromJson(JsonReader reader) throws IOException {
|
||||
return reader.nextString().toUpperCase(Locale.US);
|
||||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, String value) throws IOException {
|
||||
writer.value(value.toUpperCase(Locale.US));
|
||||
}
|
||||
}.nonNull();
|
||||
|
||||
JsonReader reader = factory.newReader("[\"a\", null, \"c\"]");
|
||||
reader.beginArray();
|
||||
assertThat(toUpperCase.fromJson(reader)).isEqualTo("A");
|
||||
try {
|
||||
toUpperCase.fromJson(reader);
|
||||
fail();
|
||||
} catch (JsonDataException expected) {
|
||||
assertThat(expected).hasMessage("Unexpected null at $[1]");
|
||||
assertThat(reader.nextNull()).isNull();
|
||||
}
|
||||
assertThat(toUpperCase.fromJson(reader)).isEqualTo("C");
|
||||
reader.endArray();
|
||||
|
||||
JsonWriter writer = factory.newWriter();
|
||||
writer.beginArray();
|
||||
toUpperCase.toJson(writer, "a");
|
||||
try {
|
||||
toUpperCase.toJson(writer, null);
|
||||
fail();
|
||||
} catch (JsonDataException expected) {
|
||||
assertThat(expected).hasMessage("Unexpected null at $[1]");
|
||||
writer.nullValue();
|
||||
}
|
||||
toUpperCase.toJson(writer, "c");
|
||||
writer.endArray();
|
||||
assertThat(factory.json()).isEqualTo("[\"A\",null,\"C\"]");
|
||||
}
|
||||
|
||||
@Test public void failOnUnknown() throws Exception {
|
||||
JsonAdapter<String> alwaysSkip = new JsonAdapter<String>() {
|
||||
@Override public String fromJson(JsonReader reader) throws IOException {
|
||||
|
@ -145,6 +185,24 @@ public final class JsonAdapterTest {
|
|||
+ "]");
|
||||
}
|
||||
|
||||
@Test public void indentDisallowsNull() throws Exception {
|
||||
JsonAdapter<Object> adapter = new JsonAdapter<Object>() {
|
||||
@Override public Object fromJson(JsonReader reader) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, Object value) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
};
|
||||
try {
|
||||
adapter.indent(null);
|
||||
fail();
|
||||
} catch (NullPointerException expected) {
|
||||
assertThat(expected).hasMessage("indent == null");
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void serializeNulls() throws Exception {
|
||||
JsonAdapter<Map<String, String>> serializeNulls = new JsonAdapter<Map<String, String>>() {
|
||||
@Override public Map<String, String> fromJson(JsonReader reader) throws IOException {
|
||||
|
@ -164,4 +222,67 @@ public final class JsonAdapterTest {
|
|||
serializeNulls.toJson(writer, Collections.<String, String>singletonMap("a", null));
|
||||
assertThat(factory.json()).isEqualTo("{\"a\":null}");
|
||||
}
|
||||
|
||||
@Test public void stringDocumentMustBeFullyConsumed() throws IOException {
|
||||
JsonAdapter<String> brokenAdapter = new JsonAdapter<String>() {
|
||||
@Override public String fromJson(JsonReader reader) throws IOException {
|
||||
return "Forgot to call reader.nextString().";
|
||||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, @Nullable String value) throws IOException {
|
||||
throw new AssertionError();
|
||||
}
|
||||
};
|
||||
try {
|
||||
brokenAdapter.fromJson("\"value\"");
|
||||
fail();
|
||||
} catch (JsonDataException e) {
|
||||
assertThat(e).hasMessage("JSON document was not fully consumed.");
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void adapterFromJsonStringPeeksAtEnd() throws IOException {
|
||||
JsonAdapter<Boolean> adapter = new JsonAdapter<Boolean>() {
|
||||
@Override public Boolean fromJson(JsonReader reader) throws IOException {
|
||||
return reader.nextBoolean();
|
||||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, @Nullable Boolean value) throws IOException {
|
||||
throw new AssertionError();
|
||||
}
|
||||
};
|
||||
try {
|
||||
adapter.fromJson("true true");
|
||||
fail();
|
||||
} catch (JsonEncodingException e) {
|
||||
assertThat(e).hasMessage(
|
||||
"Use JsonReader.setLenient(true) to accept malformed JSON at path $");
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void lenientAdapterFromJsonStringDoesNotPeekAtEnd() throws IOException {
|
||||
JsonAdapter<Boolean> adapter = new JsonAdapter<Boolean>() {
|
||||
@Override public Boolean fromJson(JsonReader reader) throws IOException {
|
||||
return reader.nextBoolean();
|
||||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, @Nullable Boolean value) throws IOException {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}.lenient();
|
||||
assertThat(adapter.fromJson("true true")).isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test public void adaptersDelegateLeniency() throws IOException {
|
||||
JsonAdapter<Boolean> adapter = new JsonAdapter<Boolean>() {
|
||||
@Override public Boolean fromJson(JsonReader reader) throws IOException {
|
||||
return reader.nextBoolean();
|
||||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, @Nullable Boolean value) throws IOException {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}.lenient().nonNull();
|
||||
assertThat(adapter.fromJson("true true")).isEqualTo(true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,9 +96,38 @@ abstract class JsonCodecFactory {
|
|||
}
|
||||
};
|
||||
|
||||
final JsonCodecFactory valuePeek = new JsonCodecFactory() {
|
||||
@Override public JsonReader newReader(String json) throws IOException {
|
||||
return value.newReader(json).peekJson();
|
||||
}
|
||||
|
||||
// TODO(jwilson): fix precision checks and delete his method.
|
||||
@Override boolean implementsStrictPrecision() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override JsonWriter newWriter() {
|
||||
return value.newWriter();
|
||||
}
|
||||
|
||||
@Override String json() {
|
||||
return value.json();
|
||||
}
|
||||
|
||||
// TODO(jwilson): support BigDecimal and BigInteger and delete his method.
|
||||
@Override boolean supportsBigNumbers() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
return "ValuePeek";
|
||||
}
|
||||
};
|
||||
|
||||
return Arrays.asList(
|
||||
new Object[] { utf8 },
|
||||
new Object[] { value });
|
||||
new Object[] { value },
|
||||
new Object[] { valuePeek });
|
||||
}
|
||||
|
||||
abstract JsonReader newReader(String json) throws IOException;
|
||||
|
|
|
@ -332,8 +332,16 @@ public final class JsonQualifiersTest {
|
|||
moshi.adapter(StringAndFooString.class);
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
assertThat(expected).hasMessage("No @FromJson adapter for class java.lang.String "
|
||||
assertThat(expected).hasMessage("No @FromJson adapter for class java.lang.String annotated "
|
||||
+ "[@com.squareup.moshi.JsonQualifiersTest$FooPrefix()]"
|
||||
+ "\nfor class java.lang.String b"
|
||||
+ "\nfor class com.squareup.moshi.JsonQualifiersTest$StringAndFooString");
|
||||
assertThat(expected).hasCauseExactlyInstanceOf(IllegalArgumentException.class);
|
||||
assertThat(expected.getCause()).hasMessage("No @FromJson adapter for class java.lang.String "
|
||||
+ "annotated [@com.squareup.moshi.JsonQualifiersTest$FooPrefix()]");
|
||||
assertThat(expected.getCause()).hasCauseExactlyInstanceOf(IllegalArgumentException.class);
|
||||
assertThat(expected.getCause().getCause()).hasMessage("No next JsonAdapter for class "
|
||||
+ "java.lang.String annotated [@com.squareup.moshi.JsonQualifiersTest$FooPrefix()]");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -353,8 +361,16 @@ public final class JsonQualifiersTest {
|
|||
moshi.adapter(StringAndFooString.class);
|
||||
fail();
|
||||
} catch (IllegalArgumentException expected) {
|
||||
assertThat(expected).hasMessage("No @ToJson adapter for class java.lang.String "
|
||||
assertThat(expected).hasMessage("No @ToJson adapter for class java.lang.String annotated "
|
||||
+ "[@com.squareup.moshi.JsonQualifiersTest$FooPrefix()]"
|
||||
+ "\nfor class java.lang.String b"
|
||||
+ "\nfor class com.squareup.moshi.JsonQualifiersTest$StringAndFooString");
|
||||
assertThat(expected).hasCauseExactlyInstanceOf(IllegalArgumentException.class);
|
||||
assertThat(expected.getCause()).hasMessage("No @ToJson adapter for class java.lang.String "
|
||||
+ "annotated [@com.squareup.moshi.JsonQualifiersTest$FooPrefix()]");
|
||||
assertThat(expected.getCause()).hasCauseExactlyInstanceOf(IllegalArgumentException.class);
|
||||
assertThat(expected.getCause().getCause()).hasMessage("No next JsonAdapter for class "
|
||||
+ "java.lang.String annotated [@com.squareup.moshi.JsonQualifiersTest$FooPrefix()]");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ public final class JsonReaderPathTest {
|
|||
return JsonCodecFactory.factories();
|
||||
}
|
||||
|
||||
@SuppressWarnings("CheckReturnValue")
|
||||
@Test public void path() throws IOException {
|
||||
JsonReader reader = factory.newReader("{\"a\":[2,true,false,null,\"b\",{\"c\":\"d\"},[3]]}");
|
||||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
|
@ -114,6 +115,7 @@ public final class JsonReaderPathTest {
|
|||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
}
|
||||
|
||||
@SuppressWarnings("CheckReturnValue")
|
||||
@Test public void objectPath() throws IOException {
|
||||
JsonReader reader = factory.newReader("{\"a\":1,\"b\":2}");
|
||||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
|
@ -154,6 +156,7 @@ public final class JsonReaderPathTest {
|
|||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
}
|
||||
|
||||
@SuppressWarnings("CheckReturnValue")
|
||||
@Test public void arrayPath() throws IOException {
|
||||
JsonReader reader = factory.newReader("[1,2]");
|
||||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
|
@ -212,6 +215,7 @@ public final class JsonReaderPathTest {
|
|||
assertThat(reader.getPath()).isEqualTo("$.null");
|
||||
}
|
||||
|
||||
@SuppressWarnings("CheckReturnValue")
|
||||
@Test public void skipObjectValues() throws IOException {
|
||||
JsonReader reader = factory.newReader("{\"a\":1,\"b\":2}");
|
||||
reader.beginObject();
|
||||
|
|
|
@ -33,10 +33,12 @@ import static com.squareup.moshi.JsonReader.Token.STRING;
|
|||
import static com.squareup.moshi.TestUtil.repeat;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
@SuppressWarnings("CheckReturnValue")
|
||||
public final class JsonReaderTest {
|
||||
@Parameter public JsonCodecFactory factory;
|
||||
|
||||
|
@ -852,6 +854,21 @@ public final class JsonReaderTest {
|
|||
reader.endArray();
|
||||
}
|
||||
|
||||
@Test public void selectStringWithoutString() throws IOException {
|
||||
JsonReader.Options numbers = JsonReader.Options.of("1", "2.0", "true", "4");
|
||||
|
||||
JsonReader reader = newReader("[0, 2.0, true, \"4\"]");
|
||||
reader.beginArray();
|
||||
assertThat(reader.selectString(numbers)).isEqualTo(-1);
|
||||
reader.skipValue();
|
||||
assertThat(reader.selectString(numbers)).isEqualTo(-1);
|
||||
reader.skipValue();
|
||||
assertThat(reader.selectString(numbers)).isEqualTo(-1);
|
||||
reader.skipValue();
|
||||
assertThat(reader.selectString(numbers)).isEqualTo(3);
|
||||
reader.endArray();
|
||||
}
|
||||
|
||||
@Test public void stringToNumberCoersion() throws Exception {
|
||||
JsonReader reader = newReader("[\"0\", \"9223372036854775807\", \"1.5\"]");
|
||||
reader.beginArray();
|
||||
|
@ -945,4 +962,227 @@ public final class JsonReaderTest {
|
|||
assertThat(value).isEqualTo(
|
||||
Collections.singletonMap("pizzas", Arrays.asList("cheese", "pepperoni")));
|
||||
}
|
||||
|
||||
@Test public void skipName() throws IOException {
|
||||
JsonReader reader = newReader("{\"a\":1}");
|
||||
reader.beginObject();
|
||||
reader.skipName();
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.NUMBER);
|
||||
reader.skipValue();
|
||||
reader.endObject();
|
||||
}
|
||||
|
||||
@Test public void skipNameOnValueFails() throws IOException {
|
||||
JsonReader reader = newReader("1");
|
||||
try {
|
||||
reader.skipName();
|
||||
fail();
|
||||
} catch (JsonDataException expected) {
|
||||
}
|
||||
assertThat(reader.nextInt()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test public void emptyDocumentHasNextReturnsFalse() throws IOException {
|
||||
JsonReader reader = newReader("1");
|
||||
reader.readJsonValue();
|
||||
assertThat(reader.hasNext()).isFalse();
|
||||
}
|
||||
|
||||
@Test public void skipValueAtEndOfObjectFails() throws IOException {
|
||||
JsonReader reader = newReader("{}");
|
||||
reader.beginObject();
|
||||
try {
|
||||
reader.skipValue();
|
||||
fail();
|
||||
} catch (JsonDataException expected) {
|
||||
assertThat(expected).hasMessage("Expected a value but was END_OBJECT at path $.");
|
||||
}
|
||||
reader.endObject();
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||
}
|
||||
|
||||
@Test public void skipValueAtEndOfArrayFails() throws IOException {
|
||||
JsonReader reader = newReader("[]");
|
||||
reader.beginArray();
|
||||
try {
|
||||
reader.skipValue();
|
||||
fail();
|
||||
} catch (JsonDataException expected) {
|
||||
assertThat(expected).hasMessage("Expected a value but was END_ARRAY at path $[0]");
|
||||
}
|
||||
reader.endArray();
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||
}
|
||||
|
||||
@Test public void skipValueAtEndOfDocumentFails() throws IOException {
|
||||
JsonReader reader = newReader("1");
|
||||
reader.nextInt();
|
||||
try {
|
||||
reader.skipValue();
|
||||
fail();
|
||||
} catch (JsonDataException expected) {
|
||||
assertThat(expected).hasMessage("Expected a value but was END_DOCUMENT at path $");
|
||||
}
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||
}
|
||||
|
||||
@Test public void basicPeekJson() throws IOException {
|
||||
JsonReader reader = newReader("{\"a\":12,\"b\":[34,56],\"c\":78}");
|
||||
reader.beginObject();
|
||||
assertThat(reader.nextName()).isEqualTo("a");
|
||||
assertThat(reader.nextInt()).isEqualTo(12);
|
||||
assertThat(reader.nextName()).isEqualTo("b");
|
||||
reader.beginArray();
|
||||
assertThat(reader.nextInt()).isEqualTo(34);
|
||||
|
||||
// Peek.
|
||||
JsonReader peekReader = reader.peekJson();
|
||||
assertThat(peekReader.nextInt()).isEqualTo(56);
|
||||
peekReader.endArray();
|
||||
assertThat(peekReader.nextName()).isEqualTo("c");
|
||||
assertThat(peekReader.nextInt()).isEqualTo(78);
|
||||
peekReader.endObject();
|
||||
assertThat(peekReader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||
|
||||
// Read again.
|
||||
assertThat(reader.nextInt()).isEqualTo(56);
|
||||
reader.endArray();
|
||||
assertThat(reader.nextName()).isEqualTo("c");
|
||||
assertThat(reader.nextInt()).isEqualTo(78);
|
||||
reader.endObject();
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* We have a document that requires 12 operations to read. We read it step-by-step with one real
|
||||
* reader. Before each of the real reader’s operations we create a peeking reader and let it read
|
||||
* the rest of the document.
|
||||
*/
|
||||
@Test public void peekJsonReader() throws IOException {
|
||||
JsonReader reader = newReader("[12,34,{\"a\":56,\"b\":78},90]");
|
||||
for (int i = 0; i < 12; i++) {
|
||||
readPeek12Steps(reader.peekJson(), i, 12);
|
||||
readPeek12Steps(reader, i, i + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a fragment of {@code reader}. This assumes the fixed document defined in {@link
|
||||
* #peekJsonReader} and reads a range of it on each call.
|
||||
*/
|
||||
private void readPeek12Steps(JsonReader reader, int from, int until) throws IOException {
|
||||
switch (from) {
|
||||
case 0:
|
||||
if (until == 0) break;
|
||||
reader.beginArray();
|
||||
assertThat(reader.getPath()).isEqualTo("$[0]");
|
||||
case 1:
|
||||
if (until == 1) break;
|
||||
assertThat(reader.nextInt()).isEqualTo(12);
|
||||
assertThat(reader.getPath()).isEqualTo("$[1]");
|
||||
case 2:
|
||||
if (until == 2) break;
|
||||
assertThat(reader.nextInt()).isEqualTo(34);
|
||||
assertThat(reader.getPath()).isEqualTo("$[2]");
|
||||
case 3:
|
||||
if (until == 3) break;
|
||||
reader.beginObject();
|
||||
assertThat(reader.getPath()).isEqualTo("$[2].");
|
||||
case 4:
|
||||
if (until == 4) break;
|
||||
assertThat(reader.nextName()).isEqualTo("a");
|
||||
assertThat(reader.getPath()).isEqualTo("$[2].a");
|
||||
case 5:
|
||||
if (until == 5) break;
|
||||
assertThat(reader.nextInt()).isEqualTo(56);
|
||||
assertThat(reader.getPath()).isEqualTo("$[2].a");
|
||||
case 6:
|
||||
if (until == 6) break;
|
||||
assertThat(reader.nextName()).isEqualTo("b");
|
||||
assertThat(reader.getPath()).isEqualTo("$[2].b");
|
||||
case 7:
|
||||
if (until == 7) break;
|
||||
assertThat(reader.nextInt()).isEqualTo(78);
|
||||
assertThat(reader.getPath()).isEqualTo("$[2].b");
|
||||
case 8:
|
||||
if (until == 8) break;
|
||||
reader.endObject();
|
||||
assertThat(reader.getPath()).isEqualTo("$[3]");
|
||||
case 9:
|
||||
if (until == 9) break;
|
||||
assertThat(reader.nextInt()).isEqualTo(90);
|
||||
assertThat(reader.getPath()).isEqualTo("$[4]");
|
||||
case 10:
|
||||
if (until == 10) break;
|
||||
reader.endArray();
|
||||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
case 11:
|
||||
if (until == 11) break;
|
||||
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||
assertThat(reader.getPath()).isEqualTo("$");
|
||||
}
|
||||
}
|
||||
|
||||
/** Confirm that we can peek in every state of the UTF-8 reader. */
|
||||
@Test public void peekAfterPeek() throws IOException {
|
||||
JsonReader reader = newReader(
|
||||
"[{\"a\":\"aaa\",'b':'bbb',c:c,\"d\":\"d\"},true,false,null,1,2.0]");
|
||||
reader.setLenient(true);
|
||||
readValue(reader, true);
|
||||
reader.peekJson();
|
||||
}
|
||||
|
||||
@Test public void peekAfterPromoteNameToValue() throws IOException {
|
||||
JsonReader reader = newReader("{\"a\":\"b\"}");
|
||||
reader.beginObject();
|
||||
reader.promoteNameToValue();
|
||||
assertEquals("a", reader.peekJson().nextString());
|
||||
assertEquals("a", reader.nextString());
|
||||
assertEquals("b", reader.peekJson().nextString());
|
||||
assertEquals("b", reader.nextString());
|
||||
reader.endObject();
|
||||
}
|
||||
|
||||
/** Peek a value, then read it, recursively. */
|
||||
private void readValue(JsonReader reader, boolean peekJsonFirst) throws IOException {
|
||||
JsonReader.Token token = reader.peek();
|
||||
if (peekJsonFirst) {
|
||||
readValue(reader.peekJson(), false);
|
||||
}
|
||||
|
||||
switch (token) {
|
||||
case BEGIN_ARRAY:
|
||||
reader.beginArray();
|
||||
while (reader.hasNext()) {
|
||||
readValue(reader, peekJsonFirst);
|
||||
}
|
||||
reader.peekJson().endArray();
|
||||
reader.endArray();
|
||||
break;
|
||||
case BEGIN_OBJECT:
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
assertNotNull(reader.peekJson().nextName());
|
||||
assertNotNull(reader.nextName());
|
||||
readValue(reader, peekJsonFirst);
|
||||
}
|
||||
reader.peekJson().endObject();
|
||||
reader.endObject();
|
||||
break;
|
||||
case STRING:
|
||||
reader.nextString();
|
||||
break;
|
||||
case NUMBER:
|
||||
reader.nextDouble();
|
||||
break;
|
||||
case BOOLEAN:
|
||||
reader.nextBoolean();
|
||||
break;
|
||||
case NULL:
|
||||
reader.nextNull();
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue