Compare commits
6 commits
master
...
revert-97-
Author | SHA1 | Date | |
---|---|---|---|
|
e42bb8a0f6 | ||
|
e024ddb431 | ||
|
7dc5c25a37 | ||
|
19c41eb18f | ||
|
2032353ef7 | ||
|
23135474f4 |
116 changed files with 3832 additions and 18103 deletions
|
@ -3,10 +3,10 @@
|
||||||
# Deploy a jar, source jar, and javadoc jar to Sonatype's snapshot repo.
|
# Deploy a jar, source jar, and javadoc jar to Sonatype's snapshot repo.
|
||||||
#
|
#
|
||||||
# Adapted from https://coderwall.com/p/9b_lfq and
|
# Adapted from https://coderwall.com/p/9b_lfq and
|
||||||
# https://benlimmer.com/2013/12/26/automatically-publish-javadoc-to-gh-pages-with-travis-ci/
|
# http://benlimmer.com/2013/12/26/automatically-publish-javadoc-to-gh-pages-with-travis-ci/
|
||||||
|
|
||||||
SLUG="square/moshi"
|
SLUG="square/moshi"
|
||||||
JDK="openjdk8"
|
JDK="oraclejdk8"
|
||||||
BRANCH="master"
|
BRANCH="master"
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -12,7 +12,6 @@ lib
|
||||||
target
|
target
|
||||||
pom.xml.*
|
pom.xml.*
|
||||||
release.properties
|
release.properties
|
||||||
dependency-reduced-pom.xml
|
|
||||||
|
|
||||||
.idea
|
.idea
|
||||||
*.iml
|
*.iml
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
language: java
|
language: java
|
||||||
|
|
||||||
jdk:
|
jdk:
|
||||||
- openjdk8
|
- oraclejdk7
|
||||||
|
- oraclejdk8
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
- .buildscript/deploy_snapshot.sh
|
- .buildscript/deploy_snapshot.sh
|
||||||
|
@ -18,6 +19,8 @@ branches:
|
||||||
notifications:
|
notifications:
|
||||||
email: false
|
email: false
|
||||||
|
|
||||||
|
sudo: false
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
- $HOME/.m2
|
- $HOME/.m2
|
||||||
|
|
328
CHANGELOG.md
328
CHANGELOG.md
|
@ -1,315 +1,43 @@
|
||||||
Change Log
|
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_
|
|
||||||
|
|
||||||
Moshi 1.4 is a major release that adds _JSON values_ as a core part of the library. We consider any
|
|
||||||
Java object comprised of maps, lists, strings, numbers, booleans and nulls to be a JSON value. These
|
|
||||||
are equivalent to parsed JSON objects in JavaScript, [Gson][gson]’s `JsonElement`, and
|
|
||||||
[Jackson][jackson]’s `JsonNode`. Unlike Jackson and Gson, Moshi just uses Java’s built-in types for
|
|
||||||
its values:
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr><th></th><th>JSON type</th><th>Java type</th></tr>
|
|
||||||
<tr><td>{...}</td><td>Object</td><td>Map<String, Object></th></tr>
|
|
||||||
<tr><td>[...]</td><td>Array</td><td>List<Object></th></tr>
|
|
||||||
<tr><td>"abc"</td><td>String</td><td>String</th></tr>
|
|
||||||
<tr><td>123</td><td>Number</td><td>Double, Long, or BigDecimal</th></tr>
|
|
||||||
<tr><td>true</td><td>Boolean</td><td>Boolean</th></tr>
|
|
||||||
<tr><td>null</td><td>null</td><td>null</th></tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
Moshi's new API `JsonAdapter.toJsonValue()` converts your application classes to JSON values
|
|
||||||
comprised of the above types. Symmetrically, `JsonAdapter.fromJsonValue()` converts JSON values to
|
|
||||||
your application classes.
|
|
||||||
|
|
||||||
* New: `JsonAdapter.toJsonValue()` and `fromJsonValue()`.
|
|
||||||
* New: `JsonReader.readJsonValue()` reads a JSON value from a stream.
|
|
||||||
* New: `Moshi.adapter(Type, Class<? extends Annotation>)` lets you look up the adapter for a
|
|
||||||
qualified type.
|
|
||||||
* New: `JsonAdapter.serializeNulls()` and `indent()` return JSON adapters that customize the
|
|
||||||
format of the encoded JSON.
|
|
||||||
* New: `JsonReader.selectName()` and `selectString()` optimize decoding JSON with known names and
|
|
||||||
values.
|
|
||||||
* 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
|
|
||||||
|
|
||||||
_2016-10-21_
|
|
||||||
|
|
||||||
* Fix: Don't incorrectly report invalid input when a slash character is escaped. When we tightened
|
|
||||||
our invalid escape handling we missed the one character that is valid both escaped `\/` and
|
|
||||||
unescaped `/`.
|
|
||||||
|
|
||||||
## Version 1.3.0
|
|
||||||
|
|
||||||
_2016-10-15_
|
|
||||||
|
|
||||||
* New: Permit `@ToJson` and `@FromJson` methods to take any number of `JsonAdapter` parameters to
|
|
||||||
delegate to. This is supported for `@ToJson` methods that take a `JsonWriter` and `@FromJson`
|
|
||||||
methods that take a `JsonReader`.
|
|
||||||
* New: Throw `JsonEncodingException` when the incoming data is not valid JSON. Use this to
|
|
||||||
differentiate data format problems from connectivity problems.
|
|
||||||
* New: Upgrade to Okio 1.11.0.
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.squareup.okio</groupId>
|
|
||||||
<artifactId>okio</artifactId>
|
|
||||||
<version>1.11.0</version>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
* New: Omit Kotlin (`kotlin.*`) and Scala (`scala.*`) platform types when encoding objects using
|
|
||||||
their fields. This should make it easier to avoid unexpected dependencies on platform versions.
|
|
||||||
* Fix: Explicitly limit reading and writing to 31 levels of nested structure. Previously no
|
|
||||||
specific limit was enforced, but deeply nested documents would fail with either an
|
|
||||||
`ArrayIndexOutOfBoundsException` due to a bug in `JsonWriter`'s path management, or a
|
|
||||||
`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 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.
|
|
||||||
|
|
||||||
## Version 1.2.0
|
|
||||||
|
|
||||||
_2016-05-28_
|
|
||||||
|
|
||||||
* New: Take advantage of Okio's new `Options` feature when reading field names and enum values.
|
|
||||||
This has a significant impact on performance. We measured parsing performance improve from 89k
|
|
||||||
ops/sec to 140k ops/sec on one benchmark on one machine.
|
|
||||||
* New: Upgrade to Okio 1.8.0.
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.squareup.okio</groupId>
|
|
||||||
<artifactId>okio</artifactId>
|
|
||||||
<version>1.8.0</version>
|
|
||||||
</dependency>
|
|
||||||
```
|
|
||||||
|
|
||||||
* New: Support types that lack no-argument constructors objects on Android releases prior to
|
|
||||||
Gingerbread.
|
|
||||||
* Fix: Add writer value overload for boxed booleans. Autoboxing resolves boxed longs and doubles
|
|
||||||
to `value(Number)`, but a boxed boolean would otherwise resolve to value(boolean) with an
|
|
||||||
implicit call to booleanValue() which has the potential to throw NPEs.
|
|
||||||
* Fix: Be more aggressive about canonicalizing types.
|
|
||||||
|
|
||||||
## Version 1.1.0
|
|
||||||
|
|
||||||
_2016-01-19_
|
|
||||||
|
|
||||||
* New: Support [RFC 7159][rfc_7159], the latest JSON specification. This removes the constraint
|
|
||||||
that the root value must be an array or an object. It may now take any value: array, object,
|
|
||||||
string, number, boolean, or null. Previously this was only permitted if the adapter was
|
|
||||||
configured to be lenient.
|
|
||||||
* New: Enum constants may be annotated with `@Json` to customize their encoded value.
|
|
||||||
* New: Create new builder from Moshi instance with `Moshi.newBuilder()`.
|
|
||||||
* New: `Types.getRawType()` and `Types.collectionElementType()` APIs to assist in defining generic
|
|
||||||
type adapter factories.
|
|
||||||
|
|
||||||
## Version 1.0.0
|
## Version 1.0.0
|
||||||
|
|
||||||
_2015-09-27_
|
_2015-09-27_
|
||||||
|
|
||||||
* **API Change**: Replaced `new JsonReader()` with `JsonReader.of()` and `new JsonWriter()` with
|
* **API Change**: Replaced `new JsonReader()` with `JsonReader.of()` and `new JsonWriter()` with
|
||||||
`JsonWriter.of()`. If your code calls either of these constructors it will need to be updated to
|
`JsonWriter.of()`. If your code calls either of these constructors it will need to be updated to
|
||||||
call the static factory method instead.
|
call the static factory method instead.
|
||||||
* **API Change**: Don’t throw `IOException` on `JsonAdapter.toJson(T)`. Code that calls this
|
* **API Change**: Don’t throw `IOException` on `JsonAdapter.toJson(T)`. Code that calls this method
|
||||||
method may need to be fixed to no longer catch an impossible `IOException`.
|
may need to be fixed to no longer catch an impossible `IOException`.
|
||||||
* Fix: the JSON adapter for `Object` no longer fails when encountering `null` in the stream.
|
* Fix: the JSON adapter for `Object` no longer fails when encountering `null` in the stream.
|
||||||
* New: `@Json` annotation can customize a field's name. This is particularly handy for fields
|
* New: `@Json` annotation can customize a field's name. This is particularly handy for fields whose
|
||||||
whose names are Java keywords, like `default` or `public`.
|
names are Java keywords, like `default` or `public`.
|
||||||
* New: `Rfc3339DateJsonAdapter` converts between a `java.util.Date` and a string formatted with
|
* New: `Rfc3339DateJsonAdapter` converts between a `java.util.Date` and a string formatted with
|
||||||
RFC 3339 (like `2015-09-26T18:23:50.250Z`). This class is in the new `moshi-adapters`
|
RFC 3339 (like `2015-09-26T18:23:50.250Z`). This class is in the new `moshi-adapters` subproject.
|
||||||
subproject. You will need to register this adapter if you want this date formatting behavior.
|
You will need to register this adapter if you want this date formatting behavior. See it in
|
||||||
See it in action in the [dates example][dates_example].
|
action in the [dates example][dates_example].
|
||||||
* New: `Moshi.adapter()` keeps a cache of all created adapters. For best efficiency, application
|
* New: `Moshi.adapter()` keeps a cache of all created adapters. For best efficiency, application
|
||||||
code should keep a reference to required adapters in a field.
|
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
|
* 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.
|
`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 unknown value is
|
* New: `JsonAdapter.failOnUnknown()` returns a new JSON adapter that throws if an unknonw value is
|
||||||
encountered on the stream. Use this in development and debug builds to detect typos in field
|
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.
|
names. This feature shouldn’t be used in production because it makes migrations very difficult.
|
||||||
|
|
||||||
## Version 0.9.0
|
## Version 0.9.0
|
||||||
|
|
||||||
_2015-06-16_
|
_2015-06-16_
|
||||||
|
|
||||||
* Databinding for primitive types, strings, enums, arrays, collections, and maps.
|
* Databinding for primitive types, strings, enums, arrays, collections, and maps.
|
||||||
* Databinding for plain old Java objects.
|
* Databinding for plain old Java objects.
|
||||||
* [JSONPath](http://goessner.net/articles/JsonPath/) support for both `JsonReader` and
|
* [JSONPath](http://goessner.net/articles/JsonPath/) support for both `JsonReader` and
|
||||||
`JsonWriter`.
|
`JsonWriter`.
|
||||||
* Throw `JsonDataException` when there’s a data binding problem.
|
* Throw `JsonDataException` when there’s a data binding problem.
|
||||||
* Adapter methods: `@ToJson` and `@FromJson`.
|
* Adapter methods: `@ToJson` and `@FromJson`.
|
||||||
* Qualifier annotations: `@JsonQualifier` to permit different type adapters for the same Java
|
* Qualifier annotations: `@JsonQualifier` to permit different type adapters for the same Java type.
|
||||||
type.
|
* Imported code from Gson: `JsonReader`, `JsonWriter`. Also some internal classes:
|
||||||
* Imported code from Gson: `JsonReader`, `JsonWriter`. Also some internal classes:
|
`LinkedHashTreeMap` for hash-collision avoidance and `Types` for typesafe databinding.
|
||||||
`LinkedHashTreeMap` for hash-collision avoidance and `Types` for typesafe databinding.
|
|
||||||
|
|
||||||
|
|
||||||
[dates_example]: https://github.com/square/moshi/blob/master/examples/src/main/java/com/squareup/moshi/recipes/ReadAndWriteRfc3339Dates.java
|
[dates_example]: https://github.com/square/moshi/blob/master/examples/src/main/java/com/squareup/moshi/recipes/ReadAndWriteRfc3339Dates.java
|
||||||
[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
|
|
||||||
|
|
443
README.md
443
README.md
|
@ -120,7 +120,7 @@ Moshi moshi = new Moshi.Builder()
|
||||||
.build();
|
.build();
|
||||||
```
|
```
|
||||||
|
|
||||||
Voilà:
|
Voila:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
@ -132,146 +132,6 @@ Voilà:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Another example
|
|
||||||
|
|
||||||
Note that the method annotated with `@FromJson` does not need to take a String as an argument.
|
|
||||||
Rather it can take input of any type and Moshi will first parse the JSON to an object of that type
|
|
||||||
and then use the `@FromJson` method to produce the desired final value. Conversely, the method
|
|
||||||
annotated with `@ToJson` does not have to produce a String.
|
|
||||||
|
|
||||||
Assume, for example, that we have to parse a JSON in which the date and time of an event are
|
|
||||||
represented as two separate strings.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"title": "Blackjack tournament",
|
|
||||||
"begin_date": "20151010",
|
|
||||||
"begin_time": "17:04"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
We would like to combine these two fields into one string to facilitate the date parsing at a
|
|
||||||
later point. Also, we would like to have all variable names in CamelCase. Therefore, the `Event`
|
|
||||||
class we want Moshi to produce like this:
|
|
||||||
|
|
||||||
```java
|
|
||||||
class Event {
|
|
||||||
String title;
|
|
||||||
String beginDateAndTime;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Instead of manually parsing the JSON line per line (which we could also do) we can have Moshi do the
|
|
||||||
transformation automatically. We simply define another class `EventJson` that directly corresponds
|
|
||||||
to the JSON structure:
|
|
||||||
|
|
||||||
```java
|
|
||||||
class EventJson {
|
|
||||||
String title;
|
|
||||||
String begin_date;
|
|
||||||
String begin_time;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
And another class with the appropriate `@FromJson` and `@ToJson` methods that are telling Moshi how
|
|
||||||
to convert an `EventJson` to an `Event` and back. Now, whenever we are asking Moshi to parse a JSON
|
|
||||||
to an `Event` it will first parse it to an `EventJson` as an intermediate step. Conversely, to
|
|
||||||
serialize an `Event` Moshi will first create an `EventJson` object and then serialize that object as
|
|
||||||
usual.
|
|
||||||
|
|
||||||
```java
|
|
||||||
class EventJsonAdapter {
|
|
||||||
@FromJson Event eventFromJson(EventJson eventJson) {
|
|
||||||
Event event = new Event();
|
|
||||||
event.title = eventJson.title;
|
|
||||||
event.beginDateAndTime = eventJson.begin_date + " " + eventJson.begin_time;
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ToJson EventJson eventToJson(Event event) {
|
|
||||||
EventJson json = new EventJson();
|
|
||||||
json.title = event.title;
|
|
||||||
json.begin_date = event.beginDateAndTime.substring(0, 8);
|
|
||||||
json.begin_time = event.beginDateAndTime.substring(9, 14);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Again we register the adapter with Moshi.
|
|
||||||
|
|
||||||
```java
|
|
||||||
Moshi moshi = new Moshi.Builder()
|
|
||||||
.add(new EventJsonAdapter())
|
|
||||||
.build();
|
|
||||||
```
|
|
||||||
|
|
||||||
We can now use Moshi to parse the JSON directly to an `Event`.
|
|
||||||
|
|
||||||
```java
|
|
||||||
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:
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"rank": "4",
|
|
||||||
"suit": "CLUBS"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rank": "A",
|
|
||||||
"suit": "HEARTS"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
We can now use Moshi to parse the JSON string into a `List<Card>`.
|
|
||||||
|
|
||||||
```java
|
|
||||||
String cardsJsonResponse = ...;
|
|
||||||
Type type = Types.newParameterizedType(List.class, Card.class);
|
|
||||||
JsonAdapter<List<Card>> adapter = moshi.adapter(type);
|
|
||||||
List<Card> cards = adapter.fromJson(cardsJsonResponse);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Fails Gracefully
|
### Fails Gracefully
|
||||||
|
|
||||||
Automatic databinding almost feels like magic. But unlike the black magic that typically accompanies
|
Automatic databinding almost feels like magic. But unlike the black magic that typically accompanies
|
||||||
|
@ -316,310 +176,28 @@ But the two libraries have a few important differences:
|
||||||
encoded in HTML without additional escaping. Moshi encodes it naturally (as `=`) and assumes that
|
encoded in HTML without additional escaping. Moshi encodes it naturally (as `=`) and assumes that
|
||||||
the HTML encoder – if there is one – will do its job.
|
the HTML encoder – if there is one – will do its job.
|
||||||
|
|
||||||
### Custom field names with @Json
|
|
||||||
|
|
||||||
Moshi works best when your JSON objects and Java objects have the same structure. But when they
|
|
||||||
don't, Moshi has annotations to customize data binding.
|
|
||||||
|
|
||||||
Use `@Json` to specify how Java fields map to JSON names. This is necessary when the JSON name
|
|
||||||
contains spaces or other characters that aren’t permitted in Java field names. For example, this
|
|
||||||
JSON has a field name containing a space:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"username": "jesse",
|
|
||||||
"lucky number": 32
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
With `@Json` its corresponding Java class is easy:
|
|
||||||
|
|
||||||
```java
|
|
||||||
class Player {
|
|
||||||
String username;
|
|
||||||
@Json(name = "lucky number") int luckyNumber;
|
|
||||||
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Because JSON field names are always defined with their Java fields, Moshi makes it easy to find
|
|
||||||
fields when navigating between Java and JSON.
|
|
||||||
|
|
||||||
### Alternate type adapters with @JsonQualifier
|
|
||||||
|
|
||||||
Use `@JsonQualifier` to customize how a type is encoded for some fields without changing its
|
|
||||||
encoding everywhere. This works similarly to the qualifier annotations in dependency injection
|
|
||||||
tools like Dagger and Guice.
|
|
||||||
|
|
||||||
Here’s a JSON message with two integers and a color:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"width": 1024,
|
|
||||||
"height": 768,
|
|
||||||
"color": "#ff0000"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
By convention, Android programs also use `int` for colors:
|
|
||||||
|
|
||||||
```java
|
|
||||||
class Rectangle {
|
|
||||||
int width;
|
|
||||||
int height;
|
|
||||||
int color;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
But if we encoded the above Java class as JSON, the color isn't encoded properly!
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"width": 1024,
|
|
||||||
"height": 768,
|
|
||||||
"color": 16711680
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The fix is to define a qualifier annotation, itself annotated `@JsonQualifier`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Retention(RUNTIME)
|
|
||||||
@JsonQualifier
|
|
||||||
public @interface HexColor {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Next apply this `@HexColor` annotation to the appropriate field:
|
|
||||||
|
|
||||||
```java
|
|
||||||
class Rectangle {
|
|
||||||
int width;
|
|
||||||
int height;
|
|
||||||
@HexColor int color;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
And finally define a type adapter to handle it:
|
|
||||||
|
|
||||||
```java
|
|
||||||
/** Converts strings like #ff0000 to the corresponding color ints. */
|
|
||||||
class ColorAdapter {
|
|
||||||
@ToJson String toJson(@HexColor int rgb) {
|
|
||||||
return String.format("#%06x", rgb);
|
|
||||||
}
|
|
||||||
|
|
||||||
@FromJson @HexColor int fromJson(String rgb) {
|
|
||||||
return Integer.parseInt(rgb.substring(1), 16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Use `@JsonQualifier` when you need different JSON encodings for the same type. Most programs
|
|
||||||
shouldn’t need this `@JsonQualifier`, but it’s very handy for those that do.
|
|
||||||
|
|
||||||
### Omit fields with `transient`
|
|
||||||
|
|
||||||
Some models declare fields that shouldn’t be included in JSON. For example, suppose our blackjack
|
|
||||||
hand has a `total` field with the sum of the cards:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public final class BlackjackHand {
|
|
||||||
private int total;
|
|
||||||
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
By default, all fields are emitted when encoding JSON, and all fields are accepted when decoding
|
|
||||||
JSON. Prevent a field from being included by adding Java’s `transient` keyword:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public final class BlackjackHand {
|
|
||||||
private transient int total;
|
|
||||||
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Transient fields are omitted when writing JSON. When reading JSON, the field is skipped even if the
|
|
||||||
JSON contains a value for the field. Instead it will get a default value.
|
|
||||||
|
|
||||||
|
|
||||||
### Default Values & Constructors
|
|
||||||
|
|
||||||
When reading JSON that is missing a field, Moshi relies on the the Java or Android runtime to assign
|
|
||||||
the field’s value. Which value it uses depends on whether the class has a no-arguments constructor.
|
|
||||||
|
|
||||||
If the class has a no-arguments constructor, Moshi will call that constructor and whatever value
|
|
||||||
it assigns will be used. For example, because this class has a no-arguments constructor the `total`
|
|
||||||
field is initialized to `-1`.
|
|
||||||
|
|
||||||
```java
|
|
||||||
public final class BlackjackHand {
|
|
||||||
private int total = -1;
|
|
||||||
...
|
|
||||||
|
|
||||||
private BlackjackHand() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public BlackjackHand(Card hidden_card, List<Card> visible_cards) {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
If the class doesn’t have a no-arguments constructor, Moshi can’t assign the field’s default value,
|
|
||||||
**even if it’s specified in the field declaration**. Instead, the field’s default is always `0` for
|
|
||||||
numbers, `false` for booleans, and `null` for references. In this example, the default value of
|
|
||||||
`total` is `0`!
|
|
||||||
|
|
||||||
```java
|
|
||||||
public final class BlackjackHand {
|
|
||||||
private int total = -1;
|
|
||||||
...
|
|
||||||
|
|
||||||
public BlackjackHand(Card hidden_card, List<Card> visible_cards) {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This is surprising and is a potential source of bugs! For this reason consider defining a
|
|
||||||
no-arguments constructor in classes that you use with Moshi, using `@SuppressWarnings("unused")` to
|
|
||||||
prevent it from being inadvertently deleted later:
|
|
||||||
|
|
||||||
```java
|
|
||||||
public final class BlackjackHand {
|
|
||||||
private int total = -1;
|
|
||||||
...
|
|
||||||
|
|
||||||
@SuppressWarnings("unused") // Moshi uses this!
|
|
||||||
private BlackjackHand() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public BlackjackHand(Card hidden_card, List<Card> visible_cards) {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
||||||
--------
|
--------
|
||||||
|
|
||||||
Download [the latest JAR][dl] or depend via Maven:
|
**Moshi is under development.** The API is not final. Download [the latest .jar][dl] or depend via
|
||||||
|
Maven:
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.squareup.moshi</groupId>
|
<groupId>com.squareup.moshi</groupId>
|
||||||
<artifactId>moshi</artifactId>
|
<artifactId>moshi</artifactId>
|
||||||
<version>1.8.0</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
```
|
```
|
||||||
or Gradle:
|
or Gradle:
|
||||||
```kotlin
|
```groovy
|
||||||
implementation("com.squareup.moshi:moshi:1.8.0")
|
compile 'com.squareup.moshi:moshi:1.0.0'
|
||||||
```
|
```
|
||||||
|
|
||||||
Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap].
|
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
|
License
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
@ -638,10 +216,9 @@ License
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
[dl]: https://search.maven.org/classic/remote_content?g=com.squareup.moshi&a=moshi&v=LATEST
|
[dl]: https://search.maven.org/remote_content?g=com.squareup.moshi&a=moshi&v=LATEST
|
||||||
[snap]: https://oss.sonatype.org/content/repositories/snapshots/com/squareup/moshi/
|
[snap]: https://oss.sonatype.org/content/repositories/snapshots/com/squareup/moshi/
|
||||||
[okio]: https://github.com/square/okio/
|
[okio]: https://github.com/square/okio/
|
||||||
[okhttp]: https://github.com/square/okhttp/
|
[okhttp]: https://github.com/square/okhttp
|
||||||
[gson]: https://github.com/google/gson/
|
[gson]: https://github.com/google/gson
|
||||||
[javadoc]: https://square.github.io/moshi/1.x/moshi/
|
[javadoc]: https://square.github.io/moshi/
|
||||||
[kapt]: https://kotlinlang.org/docs/reference/kapt.html
|
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
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>
|
<parent>
|
||||||
<groupId>com.squareup.moshi</groupId>
|
<groupId>com.squareup.moshi</groupId>
|
||||||
<artifactId>moshi-parent</artifactId>
|
<artifactId>moshi-parent</artifactId>
|
||||||
<version>1.9.0-SNAPSHOT</version>
|
<version>1.1.0-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>moshi-adapters</artifactId>
|
<artifactId>moshi-adapters</artifactId>
|
||||||
|
@ -17,11 +17,6 @@
|
||||||
<artifactId>moshi</artifactId>
|
<artifactId>moshi</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>com.google.code.findbugs</groupId>
|
|
||||||
<artifactId>jsr305</artifactId>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>junit</groupId>
|
<groupId>junit</groupId>
|
||||||
<artifactId>junit</artifactId>
|
<artifactId>junit</artifactId>
|
||||||
|
@ -33,20 +28,4 @@
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</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>
|
</project>
|
||||||
|
|
|
@ -13,9 +13,8 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package com.squareup.moshi.adapters;
|
package com.squareup.moshi;
|
||||||
|
|
||||||
import com.squareup.moshi.JsonDataException;
|
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.GregorianCalendar;
|
import java.util.GregorianCalendar;
|
||||||
|
@ -24,7 +23,7 @@ import java.util.TimeZone;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Jackson’s date formatter, pruned to Moshi's needs. Forked from this file:
|
* Jackson’s date formatter, pruned to Moshi's needs. Forked from this file:
|
||||||
* https://github.com/FasterXML/jackson-databind/blob/67ebf7305f492285a8f9f4de31545f5f16fc7c3a/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java
|
* https://github.com/FasterXML/jackson-databind/blob/master/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
|
* 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
|
* friendly than using SimpleDateFormat so highly suitable if you (un)serialize lots of date
|
||||||
|
@ -191,7 +190,7 @@ final class Iso8601Utils {
|
||||||
// If we get a ParseException it'll already have the right message/offset.
|
// If we get a ParseException it'll already have the right message/offset.
|
||||||
// Other exception types can convert here.
|
// Other exception types can convert here.
|
||||||
} catch (IndexOutOfBoundsException | IllegalArgumentException e) {
|
} catch (IndexOutOfBoundsException | IllegalArgumentException e) {
|
||||||
throw new JsonDataException("Not an RFC 3339 date: " + date, e);
|
throw new JsonDataException("Not an RFC 3339 date: " + date);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,18 +19,17 @@ import java.io.IOException;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated this class moved to avoid a package name conflict in the Java Platform Module System.
|
* Formats dates using <a href="https://www.ietf.org/rfc/rfc3339.txt">RFC 3339</a>, which is
|
||||||
* The new class is {@code com.squareup.moshi.adapters.Rfc3339DateJsonAdapter}.
|
* formatted like {@code 2015-09-26T18:23:50.250Z}.
|
||||||
*/
|
*/
|
||||||
public final class Rfc3339DateJsonAdapter extends JsonAdapter<Date> {
|
public final class Rfc3339DateJsonAdapter extends JsonAdapter<Date> {
|
||||||
com.squareup.moshi.adapters.Rfc3339DateJsonAdapter delegate
|
@Override public synchronized Date fromJson(JsonReader reader) throws IOException {
|
||||||
= new com.squareup.moshi.adapters.Rfc3339DateJsonAdapter();
|
String string = reader.nextString();
|
||||||
|
return Iso8601Utils.parse(string);
|
||||||
@Override public Date fromJson(JsonReader reader) throws IOException {
|
|
||||||
return delegate.fromJson(reader);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public void toJson(JsonWriter writer, Date value) throws IOException {
|
@Override public synchronized void toJson(JsonWriter writer, Date value) throws IOException {
|
||||||
delegate.toJson(writer, value);
|
String string = Iso8601Utils.format(value);
|
||||||
|
writer.value(string);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,110 +0,0 @@
|
||||||
/*
|
|
||||||
* 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() + ")";
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,301 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 + ")";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
/*
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -13,9 +13,8 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package com.squareup.moshi.adapters;
|
package com.squareup.moshi;
|
||||||
|
|
||||||
import com.squareup.moshi.JsonAdapter;
|
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.GregorianCalendar;
|
import java.util.GregorianCalendar;
|
|
@ -1,75 +0,0 @@
|
||||||
/*
|
|
||||||
* 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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,300 +0,0 @@
|
||||||
/*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +1,9 @@
|
||||||
<?xml version="1.0"?>
|
<?xml version="1.0"?>
|
||||||
<!DOCTYPE module PUBLIC
|
<!DOCTYPE module PUBLIC
|
||||||
"-//Puppy Crawl//DTD Check Configuration 1.3//EN"
|
"-//Puppy Crawl//DTD Check Configuration 1.2//EN"
|
||||||
"http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
|
"http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
|
||||||
|
|
||||||
<module name="Checker">
|
<module name="Checker">
|
||||||
<module name="SuppressWarningsFilter"/>
|
|
||||||
<module name="NewlineAtEndOfFile"/>
|
<module name="NewlineAtEndOfFile"/>
|
||||||
<module name="FileLength"/>
|
<module name="FileLength"/>
|
||||||
<module name="FileTabCharacter"/>
|
<module name="FileTabCharacter"/>
|
||||||
|
@ -45,7 +44,7 @@
|
||||||
<module name="LocalVariableName"/>
|
<module name="LocalVariableName"/>
|
||||||
<module name="MemberName"/>
|
<module name="MemberName"/>
|
||||||
<module name="MethodName"/>
|
<module name="MethodName"/>
|
||||||
<!--<module name="PackageName"/>-->
|
<module name="PackageName"/>
|
||||||
<module name="ParameterName"/>
|
<module name="ParameterName"/>
|
||||||
<module name="StaticVariableName"/>
|
<module name="StaticVariableName"/>
|
||||||
<module name="TypeName"/>
|
<module name="TypeName"/>
|
||||||
|
@ -57,9 +56,7 @@
|
||||||
<module name="IllegalImport"/>
|
<module name="IllegalImport"/>
|
||||||
<!-- defaults to sun.* packages -->
|
<!-- defaults to sun.* packages -->
|
||||||
<module name="RedundantImport"/>
|
<module name="RedundantImport"/>
|
||||||
<module name="UnusedImports">
|
<module name="UnusedImports"/>
|
||||||
<property name="processJavadoc" value="true"/>
|
|
||||||
</module>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Checks for Size Violations. -->
|
<!-- Checks for Size Violations. -->
|
||||||
|
@ -67,9 +64,7 @@
|
||||||
<module name="LineLength">
|
<module name="LineLength">
|
||||||
<property name="max" value="100"/>
|
<property name="max" value="100"/>
|
||||||
</module>
|
</module>
|
||||||
<module name="MethodLength">
|
<module name="MethodLength"/>
|
||||||
<property name="max" value="200"/>
|
|
||||||
</module>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Checks for whitespace -->
|
<!-- Checks for whitespace -->
|
||||||
|
@ -83,15 +78,7 @@
|
||||||
<module name="ParenPad"/>
|
<module name="ParenPad"/>
|
||||||
<module name="TypecastParenPad"/>
|
<module name="TypecastParenPad"/>
|
||||||
<module name="WhitespaceAfter"/>
|
<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 -->
|
<!-- Modifier Checks -->
|
||||||
|
@ -121,7 +108,7 @@
|
||||||
<!--module name="InnerAssignment"/-->
|
<!--module name="InnerAssignment"/-->
|
||||||
<!--module name="MagicNumber"/-->
|
<!--module name="MagicNumber"/-->
|
||||||
<!--module name="MissingSwitchDefault"/-->
|
<!--module name="MissingSwitchDefault"/-->
|
||||||
<!--<module name="RedundantThrows"/>-->
|
<module name="RedundantThrows"/>
|
||||||
<module name="SimplifyBooleanExpression"/>
|
<module name="SimplifyBooleanExpression"/>
|
||||||
<module name="SimplifyBooleanReturn"/>
|
<module name="SimplifyBooleanReturn"/>
|
||||||
|
|
||||||
|
@ -140,8 +127,5 @@
|
||||||
<!--module name="FinalParameters"/-->
|
<!--module name="FinalParameters"/-->
|
||||||
<!--module name="TodoComment"/-->
|
<!--module name="TodoComment"/-->
|
||||||
<module name="UpperEll"/>
|
<module name="UpperEll"/>
|
||||||
|
|
||||||
<!-- Make the @SuppressWarnings annotations available to Checkstyle -->
|
|
||||||
<module name="SuppressWarningsHolder"/>
|
|
||||||
</module>
|
</module>
|
||||||
</module>
|
</module>
|
||||||
|
|
|
@ -6,17 +6,12 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>com.squareup.moshi</groupId>
|
<groupId>com.squareup.moshi</groupId>
|
||||||
<artifactId>moshi-parent</artifactId>
|
<artifactId>moshi-parent</artifactId>
|
||||||
<version>1.9.0-SNAPSHOT</version>
|
<version>1.1.0-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>moshi-examples</artifactId>
|
<artifactId>moshi-examples</artifactId>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
|
||||||
<groupId>com.google.code.findbugs</groupId>
|
|
||||||
<artifactId>jsr305</artifactId>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.squareup.moshi</groupId>
|
<groupId>com.squareup.moshi</groupId>
|
||||||
<artifactId>moshi</artifactId>
|
<artifactId>moshi</artifactId>
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
/*
|
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,113 +0,0 @@
|
||||||
/*
|
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
/*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2016 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.Moshi;
|
|
||||||
import com.squareup.moshi.recipes.models.Player;
|
|
||||||
|
|
||||||
public final class CustomFieldName {
|
|
||||||
public void run() throws Exception {
|
|
||||||
String json = ""
|
|
||||||
+ "{"
|
|
||||||
+ " \"username\": \"jesse\","
|
|
||||||
+ " \"lucky number\": 32"
|
|
||||||
+ "}\n";
|
|
||||||
|
|
||||||
Moshi moshi = new Moshi.Builder().build();
|
|
||||||
JsonAdapter<Player> jsonAdapter = moshi.adapter(Player.class);
|
|
||||||
|
|
||||||
Player player = jsonAdapter.fromJson(json);
|
|
||||||
System.out.println(player);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
|
||||||
new CustomFieldName().run();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2016 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.JsonQualifier;
|
|
||||||
import com.squareup.moshi.Moshi;
|
|
||||||
import com.squareup.moshi.ToJson;
|
|
||||||
import java.lang.annotation.Retention;
|
|
||||||
|
|
||||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
|
||||||
|
|
||||||
public final class CustomQualifier {
|
|
||||||
public void run() throws Exception {
|
|
||||||
String json = ""
|
|
||||||
+ "{\n"
|
|
||||||
+ " \"color\": \"#ff0000\",\n"
|
|
||||||
+ " \"height\": 768,\n"
|
|
||||||
+ " \"width\": 1024\n"
|
|
||||||
+ "}\n";
|
|
||||||
|
|
||||||
Moshi moshi = new Moshi.Builder()
|
|
||||||
.add(new ColorAdapter())
|
|
||||||
.build();
|
|
||||||
JsonAdapter<Rectangle> jsonAdapter = moshi.adapter(Rectangle.class);
|
|
||||||
|
|
||||||
Rectangle rectangle = jsonAdapter.fromJson(json);
|
|
||||||
System.out.println(rectangle);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
|
||||||
new CustomQualifier().run();
|
|
||||||
}
|
|
||||||
|
|
||||||
static class Rectangle {
|
|
||||||
int width;
|
|
||||||
int height;
|
|
||||||
@HexColor int color;
|
|
||||||
|
|
||||||
@Override public String toString() {
|
|
||||||
return String.format("%dx%d #%06x", width, height, color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Retention(RUNTIME)
|
|
||||||
@JsonQualifier
|
|
||||||
public @interface HexColor {
|
|
||||||
}
|
|
||||||
|
|
||||||
static class ColorAdapter {
|
|
||||||
@ToJson String toJson(@HexColor int rgb) {
|
|
||||||
return String.format("#%06x", rgb);
|
|
||||||
}
|
|
||||||
|
|
||||||
@FromJson @HexColor int fromJson(String rgb) {
|
|
||||||
return Integer.parseInt(rgb.substring(1), 16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.recipes;
|
|
||||||
|
|
||||||
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 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;
|
|
||||||
private final T defaultValue;
|
|
||||||
|
|
||||||
private DefaultOnDataMismatchAdapter(JsonAdapter<T> delegate, T defaultValue) {
|
|
||||||
this.delegate = delegate;
|
|
||||||
this.defaultValue = defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public T fromJson(JsonReader reader) throws IOException {
|
|
||||||
// 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 {
|
|
||||||
// Attempt to decode to the target type with the peeked reader.
|
|
||||||
result = delegate.fromJson(peeked);
|
|
||||||
} catch (JsonDataException e) {
|
|
||||||
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 {
|
|
||||||
delegate.toJson(writer, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> Factory newFactory(final Class<T> type, final T defaultValue) {
|
|
||||||
return new Factory() {
|
|
||||||
@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);
|
|
||||||
return new DefaultOnDataMismatchAdapter<>(delegate, defaultValue);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,131 +0,0 @@
|
||||||
/*
|
|
||||||
* 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() {
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.JsonAdapter;
|
|
||||||
import com.squareup.moshi.Moshi;
|
|
||||||
import com.squareup.moshi.ToJson;
|
|
||||||
|
|
||||||
public final class FromJsonWithoutStrings {
|
|
||||||
public void run() throws Exception {
|
|
||||||
// For some reason our JSON has date and time as separate fields. We will clean that up during
|
|
||||||
// parsing: Moshi will first parse the JSON directly to an EventJson and from that the
|
|
||||||
// EventJsonAdapter will create the actual Event.
|
|
||||||
String json = ""
|
|
||||||
+ "{\n"
|
|
||||||
+ " \"title\": \"Blackjack tournament\",\n"
|
|
||||||
+ " \"begin_date\": \"20151010\",\n"
|
|
||||||
+ " \"begin_time\": \"17:04\"\n"
|
|
||||||
+ "}\n";
|
|
||||||
|
|
||||||
Moshi moshi = new Moshi.Builder().add(new EventJsonAdapter()).build();
|
|
||||||
JsonAdapter<Event> jsonAdapter = moshi.adapter(Event.class);
|
|
||||||
|
|
||||||
Event event = jsonAdapter.fromJson(json);
|
|
||||||
System.out.println(event);
|
|
||||||
System.out.println(jsonAdapter.toJson(event));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
|
||||||
new FromJsonWithoutStrings().run();
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("checkstyle:membername")
|
|
||||||
private static final class EventJson {
|
|
||||||
String title;
|
|
||||||
String begin_date;
|
|
||||||
String begin_time;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class Event {
|
|
||||||
String title;
|
|
||||||
String beginDateAndTime;
|
|
||||||
|
|
||||||
@Override public String toString() {
|
|
||||||
return "Event{"
|
|
||||||
+ "title='" + title + '\''
|
|
||||||
+ ", beginDateAndTime='" + beginDateAndTime + '\''
|
|
||||||
+ '}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class EventJsonAdapter {
|
|
||||||
@FromJson Event eventFromJson(EventJson eventJson) {
|
|
||||||
Event event = new Event();
|
|
||||||
event.title = eventJson.title;
|
|
||||||
event.beginDateAndTime = eventJson.begin_date + " " + eventJson.begin_time;
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ToJson EventJson eventToJson(Event event) {
|
|
||||||
EventJson json = new EventJson();
|
|
||||||
json.title = event.title;
|
|
||||||
json.begin_date = event.beginDateAndTime.substring(0, 8);
|
|
||||||
json.begin_time = event.beginDateAndTime.substring(9, 14);
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,94 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.JsonAdapter;
|
||||||
import com.squareup.moshi.Moshi;
|
import com.squareup.moshi.Moshi;
|
||||||
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter;
|
import com.squareup.moshi.Rfc3339DateJsonAdapter;
|
||||||
import com.squareup.moshi.recipes.models.Tournament;
|
import com.squareup.moshi.recipes.models.Tournament;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
|
@ -21,7 +21,7 @@ import com.squareup.moshi.recipes.models.BlackjackHand;
|
||||||
|
|
||||||
public final class ReadJson {
|
public final class ReadJson {
|
||||||
public void run() throws Exception {
|
public void run() throws Exception {
|
||||||
String json = ""
|
String json = ""
|
||||||
+ "{\n"
|
+ "{\n"
|
||||||
+ " \"hidden_card\": {\n"
|
+ " \"hidden_card\": {\n"
|
||||||
+ " \"rank\": \"6\",\n"
|
+ " \"rank\": \"6\",\n"
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.recipes;
|
|
||||||
|
|
||||||
import com.squareup.moshi.JsonAdapter;
|
|
||||||
import com.squareup.moshi.Moshi;
|
|
||||||
import com.squareup.moshi.Types;
|
|
||||||
import com.squareup.moshi.recipes.models.Suit;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public final class RecoverFromTypeMismatch {
|
|
||||||
public void run() throws Exception {
|
|
||||||
String json = "[\"DIAMONDS\", \"STARS\", \"HEARTS\"]";
|
|
||||||
|
|
||||||
Moshi moshi = new Moshi.Builder()
|
|
||||||
.add(DefaultOnDataMismatchAdapter.newFactory(Suit.class, Suit.CLUBS))
|
|
||||||
.build();
|
|
||||||
JsonAdapter<List<Suit>> jsonAdapter = moshi.adapter(
|
|
||||||
Types.newParameterizedType(List.class, Suit.class));
|
|
||||||
|
|
||||||
List<Suit> suits = jsonAdapter.fromJson(json);
|
|
||||||
System.out.println(suits);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
|
||||||
new RecoverFromTypeMismatch().run();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,94 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.recipes;
|
|
||||||
|
|
||||||
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 com.squareup.moshi.recipes.Unwrap.EnvelopeJsonAdapter.Enveloped;
|
|
||||||
import com.squareup.moshi.recipes.models.Card;
|
|
||||||
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 Unwrap {
|
|
||||||
private Unwrap() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
|
||||||
String json = ""
|
|
||||||
+ "{\"data\":"
|
|
||||||
+ " {\n"
|
|
||||||
+ " \"rank\": \"4\",\n"
|
|
||||||
+ " \"suit\": \"CLUBS\"\n"
|
|
||||||
+ " }"
|
|
||||||
+ "}";
|
|
||||||
Moshi moshi = new Moshi.Builder().add(EnvelopeJsonAdapter.FACTORY).build();
|
|
||||||
JsonAdapter<Card> adapter = moshi.adapter(Card.class, Enveloped.class);
|
|
||||||
Card out = adapter.fromJson(json);
|
|
||||||
System.out.println(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class EnvelopeJsonAdapter extends JsonAdapter<Object> {
|
|
||||||
public static final JsonAdapter.Factory FACTORY = new Factory() {
|
|
||||||
@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) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Type envelope =
|
|
||||||
Types.newParameterizedTypeWithOwner(EnvelopeJsonAdapter.class, Envelope.class, type);
|
|
||||||
JsonAdapter<Envelope<?>> delegate = moshi.nextAdapter(this, envelope, delegateAnnotations);
|
|
||||||
return new EnvelopeJsonAdapter(delegate);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@Retention(RUNTIME) @JsonQualifier public @interface Enveloped {
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class Envelope<T> {
|
|
||||||
final T data;
|
|
||||||
|
|
||||||
Envelope(T data) {
|
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final JsonAdapter<Envelope<?>> delegate;
|
|
||||||
|
|
||||||
EnvelopeJsonAdapter(JsonAdapter<Envelope<?>> delegate) {
|
|
||||||
this.delegate = delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public Object fromJson(JsonReader reader) throws IOException {
|
|
||||||
return delegate.fromJson(reader).data;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void toJson(JsonWriter writer, Object value) throws IOException {
|
|
||||||
delegate.toJson(writer, new Envelope<>(value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -17,14 +17,13 @@ package com.squareup.moshi.recipes.models;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@SuppressWarnings("checkstyle:membername")
|
|
||||||
public final class BlackjackHand {
|
public final class BlackjackHand {
|
||||||
public final Card hidden_card;
|
public final Card hidden_card;
|
||||||
public final List<Card> visible_cards;
|
public final List<Card> visible_cards;
|
||||||
|
|
||||||
public BlackjackHand(Card hiddenCard, List<Card> visibleCards) {
|
public BlackjackHand(Card hidden_card, List<Card> visible_cards) {
|
||||||
this.hidden_card = hiddenCard;
|
this.hidden_card = hidden_card;
|
||||||
this.visible_cards = visibleCards;
|
this.visible_cards = visible_cards;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public String toString() {
|
@Override public String toString() {
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2016 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.models;
|
|
||||||
|
|
||||||
import com.squareup.moshi.Json;
|
|
||||||
|
|
||||||
public final class Player {
|
|
||||||
public final String username;
|
|
||||||
public final @Json(name = "lucky number") int luckyNumber;
|
|
||||||
|
|
||||||
public Player(String username, int luckyNumber) {
|
|
||||||
this.username = username;
|
|
||||||
this.luckyNumber = luckyNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public String toString() {
|
|
||||||
return username + " gets lucky with " + luckyNumber;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
/** Moshi code samples. */
|
|
||||||
@javax.annotation.ParametersAreNonnullByDefault
|
|
||||||
package com.squareup.moshi.recipes;
|
|
|
@ -1,205 +0,0 @@
|
||||||
<?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>
|
|
|
@ -1,16 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,327 +0,0 @@
|
||||||
/*
|
|
||||||
* 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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,71 +0,0 @@
|
||||||
/*
|
|
||||||
* 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,97 +0,0 @@
|
||||||
/*
|
|
||||||
* 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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,128 +0,0 @@
|
||||||
/*
|
|
||||||
* 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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
/*
|
|
||||||
* 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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
/*
|
|
||||||
* 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
/*
|
|
||||||
* 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
|
|
||||||
)
|
|
|
@ -1,167 +0,0 @@
|
||||||
/*
|
|
||||||
* 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
|
|
||||||
}
|
|
|
@ -1,229 +0,0 @@
|
||||||
/*
|
|
||||||
* 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,114 +0,0 @@
|
||||||
/*
|
|
||||||
* 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
/*
|
|
||||||
* 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
/*
|
|
||||||
* 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")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,125 +0,0 @@
|
||||||
/*
|
|
||||||
* 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)
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
/*
|
|
||||||
* 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;
|
|
||||||
}
|
|
|
@ -1,381 +0,0 @@
|
||||||
/*
|
|
||||||
* 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")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,193 +0,0 @@
|
||||||
/*
|
|
||||||
* 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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
/*
|
|
||||||
* 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
|
|
||||||
)
|
|
|
@ -1,46 +0,0 @@
|
||||||
/*
|
|
||||||
* 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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,137 +0,0 @@
|
||||||
<?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>
|
|
|
@ -1,16 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,25 +0,0 @@
|
||||||
/*
|
|
||||||
* 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()
|
|
|
@ -1,262 +0,0 @@
|
||||||
/*
|
|
||||||
* 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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
-keep class kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoaderImpl
|
|
||||||
|
|
||||||
-keepclassmembers class kotlin.Metadata {
|
|
||||||
public <methods>;
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
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()"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,191 +0,0 @@
|
||||||
<?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>
|
|
|
@ -1,16 +0,0 @@
|
||||||
<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
|
@ -1,32 +0,0 @@
|
||||||
/*
|
|
||||||
* 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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
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>
|
<parent>
|
||||||
<groupId>com.squareup.moshi</groupId>
|
<groupId>com.squareup.moshi</groupId>
|
||||||
<artifactId>moshi-parent</artifactId>
|
<artifactId>moshi-parent</artifactId>
|
||||||
<version>1.9.0-SNAPSHOT</version>
|
<version>1.1.0-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>moshi</artifactId>
|
<artifactId>moshi</artifactId>
|
||||||
|
@ -17,11 +17,6 @@
|
||||||
<groupId>com.squareup.okio</groupId>
|
<groupId>com.squareup.okio</groupId>
|
||||||
<artifactId>okio</artifactId>
|
<artifactId>okio</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>com.google.code.findbugs</groupId>
|
|
||||||
<artifactId>jsr305</artifactId>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>junit</groupId>
|
<groupId>junit</groupId>
|
||||||
<artifactId>junit</artifactId>
|
<artifactId>junit</artifactId>
|
||||||
|
@ -33,31 +28,4 @@
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</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>
|
</project>
|
||||||
|
|
|
@ -15,32 +15,25 @@
|
||||||
*/
|
*/
|
||||||
package com.squareup.moshi;
|
package com.squareup.moshi;
|
||||||
|
|
||||||
import com.squareup.moshi.internal.Util;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.ParameterizedType;
|
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import javax.annotation.Nullable;
|
|
||||||
|
|
||||||
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 {
|
final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
||||||
private final List<AdapterMethod> toAdapters;
|
private final List<AdapterMethod> toAdapters;
|
||||||
private final List<AdapterMethod> fromAdapters;
|
private final List<AdapterMethod> fromAdapters;
|
||||||
|
|
||||||
AdapterMethodsFactory(List<AdapterMethod> toAdapters, List<AdapterMethod> fromAdapters) {
|
public AdapterMethodsFactory(List<AdapterMethod> toAdapters, List<AdapterMethod> fromAdapters) {
|
||||||
this.toAdapters = toAdapters;
|
this.toAdapters = toAdapters;
|
||||||
this.fromAdapters = fromAdapters;
|
this.fromAdapters = fromAdapters;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public @Nullable JsonAdapter<?> create(
|
@Override public JsonAdapter<?> create(
|
||||||
final Type type, final Set<? extends Annotation> annotations, final Moshi moshi) {
|
final Type type, final Set<? extends Annotation> annotations, final Moshi moshi) {
|
||||||
final AdapterMethod toAdapter = get(toAdapters, type, annotations);
|
final AdapterMethod toAdapter = get(toAdapters, type, annotations);
|
||||||
final AdapterMethod fromAdapter = get(fromAdapters, type, annotations);
|
final AdapterMethod fromAdapter = get(fromAdapters, type, annotations);
|
||||||
|
@ -53,17 +46,14 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
String missingAnnotation = toAdapter == null ? "@ToJson" : "@FromJson";
|
String missingAnnotation = toAdapter == null ? "@ToJson" : "@FromJson";
|
||||||
throw new IllegalArgumentException("No " + missingAnnotation + " adapter for "
|
throw new IllegalArgumentException("No " + missingAnnotation + " adapter for "
|
||||||
+ typeAnnotatedWithAnnotations(type, annotations), e);
|
+ type + " annotated " + annotations);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
delegate = null;
|
delegate = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toAdapter != null) toAdapter.bind(moshi, this);
|
|
||||||
if (fromAdapter != null) fromAdapter.bind(moshi, this);
|
|
||||||
|
|
||||||
return new JsonAdapter<Object>() {
|
return new JsonAdapter<Object>() {
|
||||||
@Override public void toJson(JsonWriter writer, @Nullable Object value) throws IOException {
|
@Override public void toJson(JsonWriter writer, Object value) throws IOException {
|
||||||
if (toAdapter == null) {
|
if (toAdapter == null) {
|
||||||
delegate.toJson(writer, value);
|
delegate.toJson(writer, value);
|
||||||
} else if (!toAdapter.nullable && value == null) {
|
} else if (!toAdapter.nullable && value == null) {
|
||||||
|
@ -71,15 +61,16 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
toAdapter.toJson(moshi, writer, value);
|
toAdapter.toJson(moshi, writer, value);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new AssertionError();
|
||||||
} catch (InvocationTargetException e) {
|
} catch (InvocationTargetException e) {
|
||||||
Throwable cause = e.getCause();
|
if (e.getCause() instanceof IOException) throw (IOException) e.getCause();
|
||||||
if (cause instanceof IOException) throw (IOException) cause;
|
throw new JsonDataException(e.getCause() + " at " + writer.getPath());
|
||||||
throw new JsonDataException(cause + " at " + writer.getPath(), cause);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public @Nullable Object fromJson(JsonReader reader) throws IOException {
|
@Override public Object fromJson(JsonReader reader) throws IOException {
|
||||||
if (fromAdapter == null) {
|
if (fromAdapter == null) {
|
||||||
return delegate.fromJson(reader);
|
return delegate.fromJson(reader);
|
||||||
} else if (!fromAdapter.nullable && reader.peek() == JsonReader.Token.NULL) {
|
} else if (!fromAdapter.nullable && reader.peek() == JsonReader.Token.NULL) {
|
||||||
|
@ -88,10 +79,11 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
return fromAdapter.fromJson(moshi, reader);
|
return fromAdapter.fromJson(moshi, reader);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
throw new AssertionError();
|
||||||
} catch (InvocationTargetException e) {
|
} catch (InvocationTargetException e) {
|
||||||
Throwable cause = e.getCause();
|
if (e.getCause() instanceof IOException) throw (IOException) e.getCause();
|
||||||
if (cause instanceof IOException) throw (IOException) cause;
|
throw new JsonDataException(e.getCause() + " at " + reader.getPath());
|
||||||
throw new JsonDataException(cause + " at " + reader.getPath(), cause);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -146,46 +138,34 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
||||||
*/
|
*/
|
||||||
static AdapterMethod toAdapter(Object adapter, Method method) {
|
static AdapterMethod toAdapter(Object adapter, Method method) {
|
||||||
method.setAccessible(true);
|
method.setAccessible(true);
|
||||||
|
Type[] parameterTypes = method.getGenericParameterTypes();
|
||||||
final Type returnType = method.getGenericReturnType();
|
final Type returnType = method.getGenericReturnType();
|
||||||
final Type[] parameterTypes = method.getGenericParameterTypes();
|
|
||||||
final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
|
|
||||||
|
|
||||||
if (parameterTypes.length >= 2
|
if (parameterTypes.length == 2
|
||||||
&& parameterTypes[0] == JsonWriter.class
|
&& parameterTypes[0] == JsonWriter.class
|
||||||
&& returnType == void.class
|
&& returnType == void.class) {
|
||||||
&& parametersAreJsonAdapters(2, parameterTypes)) {
|
// public void pointToJson(JsonWriter jsonWriter, Point point) throws Exception {
|
||||||
// void pointToJson(JsonWriter jsonWriter, Point point) {
|
Set<? extends Annotation> parameterAnnotations
|
||||||
// void pointToJson(JsonWriter jsonWriter, Point point, JsonAdapter<?> adapter, ...) {
|
= Util.jsonAnnotations(method.getParameterAnnotations()[1]);
|
||||||
Set<? extends Annotation> qualifierAnnotations = jsonAnnotations(parameterAnnotations[1]);
|
return new AdapterMethod(parameterTypes[1], parameterAnnotations, adapter, method, false) {
|
||||||
return new AdapterMethod(parameterTypes[1], qualifierAnnotations, adapter, method,
|
@Override public void toJson(Moshi moshi, JsonWriter writer, Object value)
|
||||||
parameterTypes.length, 2, true) {
|
throws IOException, InvocationTargetException, IllegalAccessException {
|
||||||
@Override public void toJson(Moshi moshi, JsonWriter writer, @Nullable Object value)
|
method.invoke(adapter, writer, value);
|
||||||
throws IOException, InvocationTargetException {
|
|
||||||
invoke(writer, value);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} else if (parameterTypes.length == 1 && returnType != void.class) {
|
} else if (parameterTypes.length == 1 && returnType != void.class) {
|
||||||
// List<Integer> pointToJson(Point point) {
|
// public List<Integer> pointToJson(Point point) throws Exception {
|
||||||
final Set<? extends Annotation> returnTypeAnnotations = jsonAnnotations(method);
|
final Set<? extends Annotation> returnTypeAnnotations = Util.jsonAnnotations(method);
|
||||||
final Set<? extends Annotation> qualifierAnnotations =
|
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
|
||||||
jsonAnnotations(parameterAnnotations[0]);
|
Set<? extends Annotation> qualifierAnnotations =
|
||||||
|
Util.jsonAnnotations(parameterAnnotations[0]);
|
||||||
boolean nullable = Util.hasNullable(parameterAnnotations[0]);
|
boolean nullable = Util.hasNullable(parameterAnnotations[0]);
|
||||||
return new AdapterMethod(parameterTypes[0], qualifierAnnotations, adapter, method,
|
return new AdapterMethod(parameterTypes[0], qualifierAnnotations, adapter, method, nullable) {
|
||||||
parameterTypes.length, 1, nullable) {
|
@Override public void toJson(Moshi moshi, JsonWriter writer, Object value)
|
||||||
private JsonAdapter<Object> delegate;
|
throws IOException, InvocationTargetException, IllegalAccessException {
|
||||||
|
JsonAdapter<Object> delegate = moshi.adapter(returnType, returnTypeAnnotations);
|
||||||
@Override public void bind(Moshi moshi, JsonAdapter.Factory factory) {
|
Object intermediate = method.invoke(adapter, value);
|
||||||
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, @Nullable Object value)
|
|
||||||
throws IOException, InvocationTargetException {
|
|
||||||
Object intermediate = invoke(value);
|
|
||||||
delegate.toJson(writer, intermediate);
|
delegate.toJson(writer, intermediate);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -194,163 +174,91 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
||||||
throw new IllegalArgumentException("Unexpected signature for " + method + ".\n"
|
throw new IllegalArgumentException("Unexpected signature for " + method + ".\n"
|
||||||
+ "@ToJson method signatures may have one of the following structures:\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) 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");
|
+ " <any access modifier> R toJson(T value) throws <any>;\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns true if {@code parameterTypes[offset..]} contains only JsonAdapters. */
|
|
||||||
private static boolean parametersAreJsonAdapters(int offset, Type[] parameterTypes) {
|
|
||||||
for (int i = offset, length = parameterTypes.length; i < length; i++) {
|
|
||||||
if (!(parameterTypes[i] instanceof ParameterizedType)) return false;
|
|
||||||
if (((ParameterizedType) parameterTypes[i]).getRawType() != JsonAdapter.class) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an object that calls a {@code method} method on {@code adapter} in service of
|
* Returns an object that calls a {@code method} method on {@code adapter} in service of
|
||||||
* converting an object from JSON.
|
* converting an object from JSON.
|
||||||
*/
|
*/
|
||||||
static AdapterMethod fromAdapter(Object adapter, Method method) {
|
static AdapterMethod fromAdapter(Object adapter, Method method) {
|
||||||
method.setAccessible(true);
|
method.setAccessible(true);
|
||||||
final Type returnType = method.getGenericReturnType();
|
|
||||||
final Set<? extends Annotation> returnTypeAnnotations = jsonAnnotations(method);
|
|
||||||
final Type[] parameterTypes = method.getGenericParameterTypes();
|
final Type[] parameterTypes = method.getGenericParameterTypes();
|
||||||
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
|
final Type returnType = method.getGenericReturnType();
|
||||||
|
|
||||||
if (parameterTypes.length >= 1
|
if (parameterTypes.length == 1
|
||||||
&& parameterTypes[0] == JsonReader.class
|
&& parameterTypes[0] == JsonReader.class
|
||||||
&& returnType != void.class
|
&& returnType != void.class) {
|
||||||
&& parametersAreJsonAdapters(1, parameterTypes)) {
|
// public Point pointFromJson(JsonReader jsonReader) throws Exception {
|
||||||
// Point pointFromJson(JsonReader jsonReader) {
|
Set<? extends Annotation> returnTypeAnnotations = Util.jsonAnnotations(method);
|
||||||
// Point pointFromJson(JsonReader jsonReader, JsonAdapter<?> adapter, ...) {
|
return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method, false) {
|
||||||
return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method,
|
|
||||||
parameterTypes.length, 1, true) {
|
|
||||||
@Override public Object fromJson(Moshi moshi, JsonReader reader)
|
@Override public Object fromJson(Moshi moshi, JsonReader reader)
|
||||||
throws IOException, InvocationTargetException {
|
throws IOException, IllegalAccessException, InvocationTargetException {
|
||||||
return invoke(reader);
|
return method.invoke(adapter, reader);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} else if (parameterTypes.length == 1 && returnType != void.class) {
|
} else if (parameterTypes.length == 1 && returnType != void.class) {
|
||||||
// Point pointFromJson(List<Integer> o) {
|
// public Point pointFromJson(List<Integer> o) throws Exception {
|
||||||
|
Set<? extends Annotation> returnTypeAnnotations = Util.jsonAnnotations(method);
|
||||||
|
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
|
||||||
final Set<? extends Annotation> qualifierAnnotations
|
final Set<? extends Annotation> qualifierAnnotations
|
||||||
= jsonAnnotations(parameterAnnotations[0]);
|
= Util.jsonAnnotations(parameterAnnotations[0]);
|
||||||
boolean nullable = Util.hasNullable(parameterAnnotations[0]);
|
boolean nullable = Util.hasNullable(parameterAnnotations[0]);
|
||||||
return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method,
|
return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method, nullable) {
|
||||||
parameterTypes.length, 1, nullable) {
|
|
||||||
JsonAdapter<Object> delegate;
|
|
||||||
|
|
||||||
@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)
|
@Override public Object fromJson(Moshi moshi, JsonReader reader)
|
||||||
throws IOException, InvocationTargetException {
|
throws IOException, IllegalAccessException, InvocationTargetException {
|
||||||
|
JsonAdapter<Object> delegate = moshi.adapter(parameterTypes[0], qualifierAnnotations);
|
||||||
Object intermediate = delegate.fromJson(reader);
|
Object intermediate = delegate.fromJson(reader);
|
||||||
return invoke(intermediate);
|
return method.invoke(adapter, intermediate);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Unexpected signature for " + method + ".\n"
|
throw new IllegalArgumentException("Unexpected signature for " + method + ".\n"
|
||||||
+ "@FromJson method signatures may have one of the following structures:\n"
|
+ "@ToJson method signatures may have one of the following structures:\n"
|
||||||
+ " <any access modifier> R fromJson(JsonReader jsonReader) throws <any>;\n"
|
+ " <any access modifier> void toJson(JsonWriter writer, T value) throws <any>;\n"
|
||||||
+ " <any access modifier> R fromJson(JsonReader jsonReader,"
|
+ " <any access modifier> R toJson(T value) throws <any>;\n");
|
||||||
+ " 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. */
|
/** Returns the matching adapter method from the list. */
|
||||||
private static @Nullable AdapterMethod get(
|
private static AdapterMethod get(
|
||||||
List<AdapterMethod> adapterMethods, Type type, Set<? extends Annotation> annotations) {
|
List<AdapterMethod> adapterMethods, Type type, Set<? extends Annotation> annotations) {
|
||||||
for (int i = 0, size = adapterMethods.size(); i < size; i++) {
|
for (int i = 0, size = adapterMethods.size(); i < size; i++) {
|
||||||
AdapterMethod adapterMethod = adapterMethods.get(i);
|
AdapterMethod adapterMethod = adapterMethods.get(i);
|
||||||
if (Types.equals(adapterMethod.type, type) && adapterMethod.annotations.equals(annotations)) {
|
if (adapterMethod.type.equals(type) && adapterMethod.annotations.equals(annotations)) {
|
||||||
return adapterMethod;
|
return adapterMethod;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract static class AdapterMethod {
|
static abstract class AdapterMethod {
|
||||||
final Type type;
|
final Type type;
|
||||||
final Set<? extends Annotation> annotations;
|
final Set<? extends Annotation> annotations;
|
||||||
final Object adapter;
|
final Object adapter;
|
||||||
final Method method;
|
final Method method;
|
||||||
final int adaptersOffset;
|
|
||||||
final JsonAdapter<?>[] jsonAdapters;
|
|
||||||
final boolean nullable;
|
final boolean nullable;
|
||||||
|
|
||||||
AdapterMethod(Type type, Set<? extends Annotation> annotations, Object adapter,
|
public AdapterMethod(Type type,
|
||||||
Method method, int parameterCount, int adaptersOffset, boolean nullable) {
|
Set<? extends Annotation> annotations, Object adapter, Method method, boolean nullable) {
|
||||||
this.type = canonicalize(type);
|
this.type = type;
|
||||||
this.annotations = annotations;
|
this.annotations = annotations;
|
||||||
this.adapter = adapter;
|
this.adapter = adapter;
|
||||||
this.method = method;
|
this.method = method;
|
||||||
this.adaptersOffset = adaptersOffset;
|
|
||||||
this.jsonAdapters = new JsonAdapter[parameterCount - adaptersOffset];
|
|
||||||
this.nullable = nullable;
|
this.nullable = nullable;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void bind(Moshi moshi, JsonAdapter.Factory factory) {
|
public void toJson(Moshi moshi, JsonWriter writer, Object value)
|
||||||
if (jsonAdapters.length > 0) {
|
throws IOException, IllegalAccessException, InvocationTargetException {
|
||||||
Type[] parameterTypes = method.getGenericParameterTypes();
|
|
||||||
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
|
|
||||||
for (int i = adaptersOffset, size = parameterTypes.length; i < size; 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, @Nullable Object value)
|
|
||||||
throws IOException, InvocationTargetException {
|
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable Object fromJson(Moshi moshi, JsonReader reader)
|
public Object fromJson(Moshi moshi, JsonReader reader)
|
||||||
throws IOException, InvocationTargetException {
|
throws IOException, IllegalAccessException, InvocationTargetException {
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Invoke the method with one fixed argument, plus any number of JSON adapter arguments. */
|
|
||||||
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);
|
|
||||||
|
|
||||||
try {
|
|
||||||
return method.invoke(adapter, args);
|
|
||||||
} catch (IllegalAccessException e) {
|
|
||||||
throw new AssertionError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Invoke the method with two fixed arguments, plus any number of JSON adapter arguments. */
|
|
||||||
protected Object invoke(@Nullable Object a1, @Nullable Object a2)
|
|
||||||
throws InvocationTargetException {
|
|
||||||
Object[] args = new Object[2 + jsonAdapters.length];
|
|
||||||
args[0] = a1;
|
|
||||||
args[1] = a2;
|
|
||||||
System.arraycopy(jsonAdapters, 0, args, 2, jsonAdapters.length);
|
|
||||||
|
|
||||||
try {
|
|
||||||
return method.invoke(adapter, args);
|
|
||||||
} catch (IllegalAccessException e) {
|
|
||||||
throw new AssertionError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,6 @@ import java.lang.reflect.Type;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import javax.annotation.Nullable;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts arrays to JSON arrays containing their converted contents. This
|
* Converts arrays to JSON arrays containing their converted contents. This
|
||||||
|
@ -30,7 +29,7 @@ import javax.annotation.Nullable;
|
||||||
*/
|
*/
|
||||||
final class ArrayJsonAdapter extends JsonAdapter<Object> {
|
final class ArrayJsonAdapter extends JsonAdapter<Object> {
|
||||||
public static final Factory FACTORY = new Factory() {
|
public static final Factory FACTORY = new Factory() {
|
||||||
@Override public @Nullable JsonAdapter<?> create(
|
@Override public JsonAdapter<?> create(
|
||||||
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||||
Type elementType = Types.arrayComponentType(type);
|
Type elementType = Types.arrayComponentType(type);
|
||||||
if (elementType == null) return null;
|
if (elementType == null) return null;
|
||||||
|
@ -70,8 +69,4 @@ final class ArrayJsonAdapter extends JsonAdapter<Object> {
|
||||||
}
|
}
|
||||||
writer.endArray();
|
writer.endArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public String toString() {
|
|
||||||
return elementAdapter + ".array()";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,6 @@
|
||||||
*/
|
*/
|
||||||
package com.squareup.moshi;
|
package com.squareup.moshi;
|
||||||
|
|
||||||
import com.squareup.moshi.internal.Util;
|
|
||||||
import java.io.ObjectInputStream;
|
|
||||||
import java.io.ObjectStreamClass;
|
import java.io.ObjectStreamClass;
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
@ -79,7 +77,7 @@ abstract class ClassFactory<T> {
|
||||||
// Not the expected version of the Oracle Java library!
|
// Not the expected version of the Oracle Java library!
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try (post-Gingerbread) Dalvik/libcore's ObjectStreamClass mechanism.
|
// Try Dalvik/libcore's ObjectStreamClass mechanism.
|
||||||
// public class ObjectStreamClass {
|
// public class ObjectStreamClass {
|
||||||
// private static native int getConstructorId(Class<?> c);
|
// private static native int getConstructorId(Class<?> c);
|
||||||
// private static native Object newInstance(Class<?> instantiationClass, int methodId);
|
// private static native Object newInstance(Class<?> instantiationClass, int methodId);
|
||||||
|
@ -104,32 +102,11 @@ abstract class ClassFactory<T> {
|
||||||
} catch (IllegalAccessException e) {
|
} catch (IllegalAccessException e) {
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
} catch (InvocationTargetException e) {
|
} catch (InvocationTargetException e) {
|
||||||
throw Util.rethrowCause(e);
|
throw new RuntimeException(e);
|
||||||
} catch (NoSuchMethodException ignored) {
|
} catch (NoSuchMethodException ignored) {
|
||||||
// Not the expected version of Dalvik/libcore!
|
// Not the expected version of Dalvik/libcore!
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try (pre-Gingerbread) Dalvik/libcore's ObjectInputStream mechanism.
|
|
||||||
// public class ObjectInputStream {
|
|
||||||
// private static native Object newInstance(
|
|
||||||
// Class<?> instantiationClass, Class<?> constructorClass);
|
|
||||||
// }
|
|
||||||
try {
|
|
||||||
final Method newInstance = ObjectInputStream.class.getDeclaredMethod(
|
|
||||||
"newInstance", Class.class, Class.class);
|
|
||||||
newInstance.setAccessible(true);
|
|
||||||
return new ClassFactory<T>() {
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@Override public T newInstance() throws InvocationTargetException, IllegalAccessException {
|
|
||||||
return (T) newInstance.invoke(null, rawType, Object.class);
|
|
||||||
}
|
|
||||||
@Override public String toString() {
|
|
||||||
return rawType.getName();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new IllegalArgumentException("cannot construct instances of " + rawType.getName());
|
throw new IllegalArgumentException("cannot construct instances of " + rawType.getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,77 +15,48 @@
|
||||||
*/
|
*/
|
||||||
package com.squareup.moshi;
|
package com.squareup.moshi;
|
||||||
|
|
||||||
import com.squareup.moshi.internal.Util;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Modifier;
|
import java.lang.reflect.Modifier;
|
||||||
import java.lang.reflect.ParameterizedType;
|
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeMap;
|
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.
|
* Emits a regular class as a JSON object by mapping Java fields to JSON object properties. Fields
|
||||||
*
|
* of classes in {@code java.*}, {@code javax.*} and {@code android.*} are omitted from both
|
||||||
* <h3>Platform Types</h3>
|
* serialization and deserialization unless they are either public or protected.
|
||||||
* Fields from platform classes are omitted from both serialization and deserialization unless
|
|
||||||
* they are either public or protected. This includes the following packages and their subpackages:
|
|
||||||
*
|
|
||||||
* <ul>
|
|
||||||
* <li>android.*
|
|
||||||
* <li>java.*
|
|
||||||
* <li>javax.*
|
|
||||||
* <li>kotlin.*
|
|
||||||
* <li>scala.*
|
|
||||||
* </ul>
|
|
||||||
*/
|
*/
|
||||||
final class ClassJsonAdapter<T> extends JsonAdapter<T> {
|
final class ClassJsonAdapter<T> extends JsonAdapter<T> {
|
||||||
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
|
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
|
||||||
@Override public @Nullable JsonAdapter<?> create(
|
@Override public JsonAdapter<?> create(
|
||||||
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||||
if (!(type instanceof Class) && !(type instanceof ParameterizedType)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Class<?> rawType = Types.getRawType(type);
|
Class<?> rawType = Types.getRawType(type);
|
||||||
if (rawType.isInterface() || rawType.isEnum()) return null;
|
if (rawType.isInterface() || rawType.isEnum()) return null;
|
||||||
|
if (isPlatformType(rawType)) {
|
||||||
|
throw new IllegalArgumentException("Platform "
|
||||||
|
+ type
|
||||||
|
+ " annotated "
|
||||||
|
+ annotations
|
||||||
|
+ " requires explicit JsonAdapter to be registered");
|
||||||
|
}
|
||||||
if (!annotations.isEmpty()) return null;
|
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.getEnclosingClass() != null && !Modifier.isStatic(rawType.getModifiers())) {
|
||||||
throw new IllegalArgumentException(
|
if (rawType.getSimpleName().isEmpty()) {
|
||||||
"Cannot serialize non-static nested class " + rawType.getName());
|
throw new IllegalArgumentException(
|
||||||
|
"Cannot serialize anonymous class " + rawType.getName());
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Cannot serialize non-static nested class " + rawType.getName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (Modifier.isAbstract(rawType.getModifiers())) {
|
if (Modifier.isAbstract(rawType.getModifiers())) {
|
||||||
throw new IllegalArgumentException("Cannot serialize abstract class " + rawType.getName());
|
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);
|
ClassFactory<Object> classFactory = ClassFactory.get(rawType);
|
||||||
Map<String, FieldBinding<?>> fields = new TreeMap<>();
|
Map<String, FieldBinding<?>> fields = new TreeMap<>();
|
||||||
|
@ -99,23 +70,22 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
|
||||||
private void createFieldBindings(
|
private void createFieldBindings(
|
||||||
Moshi moshi, Type type, Map<String, FieldBinding<?>> fieldBindings) {
|
Moshi moshi, Type type, Map<String, FieldBinding<?>> fieldBindings) {
|
||||||
Class<?> rawType = Types.getRawType(type);
|
Class<?> rawType = Types.getRawType(type);
|
||||||
boolean platformType = Util.isPlatformType(rawType);
|
boolean platformType = isPlatformType(rawType);
|
||||||
for (Field field : rawType.getDeclaredFields()) {
|
for (Field field : rawType.getDeclaredFields()) {
|
||||||
if (!includeField(platformType, field.getModifiers())) continue;
|
if (!includeField(platformType, field.getModifiers())) continue;
|
||||||
|
|
||||||
// Look up a type adapter for this type.
|
// Look up a type adapter for this type.
|
||||||
Type fieldType = resolve(type, rawType, field.getGenericType());
|
Type fieldType = Types.resolve(type, rawType, field.getGenericType());
|
||||||
Set<? extends Annotation> annotations = Util.jsonAnnotations(field);
|
Set<? extends Annotation> annotations = Util.jsonAnnotations(field);
|
||||||
String fieldName = field.getName();
|
JsonAdapter<Object> adapter = moshi.adapter(fieldType, annotations);
|
||||||
JsonAdapter<Object> adapter = moshi.adapter(fieldType, annotations, fieldName);
|
|
||||||
|
|
||||||
// Create the binding between field and JSON.
|
// Create the binding between field and JSON.
|
||||||
field.setAccessible(true);
|
field.setAccessible(true);
|
||||||
|
FieldBinding<Object> fieldBinding = new FieldBinding<>(field, adapter);
|
||||||
|
|
||||||
// Store it using the field's name. If there was already a field with this name, fail!
|
// Store it using the field's name. If there was already a field with this name, fail!
|
||||||
Json jsonAnnotation = field.getAnnotation(Json.class);
|
Json jsonAnnotation = field.getAnnotation(Json.class);
|
||||||
String name = jsonAnnotation != null ? jsonAnnotation.name() : fieldName;
|
String name = jsonAnnotation != null ? jsonAnnotation.name() : field.getName();
|
||||||
FieldBinding<Object> fieldBinding = new FieldBinding<>(name, field, adapter);
|
|
||||||
FieldBinding<?> replaced = fieldBindings.put(name, fieldBinding);
|
FieldBinding<?> replaced = fieldBindings.put(name, fieldBinding);
|
||||||
if (replaced != null) {
|
if (replaced != null) {
|
||||||
throw new IllegalArgumentException("Conflicting fields:\n"
|
throw new IllegalArgumentException("Conflicting fields:\n"
|
||||||
|
@ -125,22 +95,29 @@ 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) {
|
||||||
|
return rawType.getName().startsWith("java.")
|
||||||
|
|| rawType.getName().startsWith("javax.")
|
||||||
|
|| rawType.getName().startsWith("android.");
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns true if fields with {@code modifiers} are included in the emitted JSON. */
|
/** Returns true if fields with {@code modifiers} are included in the emitted JSON. */
|
||||||
private boolean includeField(boolean platformType, int modifiers) {
|
private boolean includeField(boolean platformType, int modifiers) {
|
||||||
if (Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers)) return false;
|
if (Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers)) return false;
|
||||||
return Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers) || !platformType;
|
return Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers)|| !platformType;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private final ClassFactory<T> classFactory;
|
private final ClassFactory<T> classFactory;
|
||||||
private final FieldBinding<?>[] fieldsArray;
|
private final Map<String, FieldBinding<?>> jsonFields;
|
||||||
private final JsonReader.Options options;
|
|
||||||
|
|
||||||
ClassJsonAdapter(ClassFactory<T> classFactory, Map<String, FieldBinding<?>> fieldsMap) {
|
private ClassJsonAdapter(ClassFactory<T> classFactory, Map<String, FieldBinding<?>> jsonFields) {
|
||||||
this.classFactory = classFactory;
|
this.classFactory = classFactory;
|
||||||
this.fieldsArray = fieldsMap.values().toArray(new FieldBinding[fieldsMap.size()]);
|
this.jsonFields = jsonFields;
|
||||||
this.options = JsonReader.Options.of(
|
|
||||||
fieldsMap.keySet().toArray(new String[fieldsMap.size()]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public T fromJson(JsonReader reader) throws IOException {
|
@Override public T fromJson(JsonReader reader) throws IOException {
|
||||||
|
@ -150,7 +127,10 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
|
||||||
} catch (InstantiationException e) {
|
} catch (InstantiationException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
} catch (InvocationTargetException e) {
|
} catch (InvocationTargetException e) {
|
||||||
throw Util.rethrowCause(e);
|
Throwable targetException = e.getTargetException();
|
||||||
|
if (targetException instanceof RuntimeException) throw (RuntimeException) targetException;
|
||||||
|
if (targetException instanceof Error) throw (Error) targetException;
|
||||||
|
throw new RuntimeException(targetException);
|
||||||
} catch (IllegalAccessException e) {
|
} catch (IllegalAccessException e) {
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
|
@ -158,13 +138,13 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
|
||||||
try {
|
try {
|
||||||
reader.beginObject();
|
reader.beginObject();
|
||||||
while (reader.hasNext()) {
|
while (reader.hasNext()) {
|
||||||
int index = reader.selectName(options);
|
String name = reader.nextName();
|
||||||
if (index == -1) {
|
FieldBinding<?> fieldBinding = jsonFields.get(name);
|
||||||
reader.skipName();
|
if (fieldBinding != null) {
|
||||||
|
fieldBinding.read(reader, result);
|
||||||
|
} else {
|
||||||
reader.skipValue();
|
reader.skipValue();
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
fieldsArray[index].read(reader, result);
|
|
||||||
}
|
}
|
||||||
reader.endObject();
|
reader.endObject();
|
||||||
return result;
|
return result;
|
||||||
|
@ -176,9 +156,9 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
|
||||||
@Override public void toJson(JsonWriter writer, T value) throws IOException {
|
@Override public void toJson(JsonWriter writer, T value) throws IOException {
|
||||||
try {
|
try {
|
||||||
writer.beginObject();
|
writer.beginObject();
|
||||||
for (FieldBinding<?> fieldBinding : fieldsArray) {
|
for (Map.Entry<String, FieldBinding<?>> entry : jsonFields.entrySet()) {
|
||||||
writer.name(fieldBinding.name);
|
writer.name(entry.getKey());
|
||||||
fieldBinding.write(writer, value);
|
entry.getValue().write(writer, value);
|
||||||
}
|
}
|
||||||
writer.endObject();
|
writer.endObject();
|
||||||
} catch (IllegalAccessException e) {
|
} catch (IllegalAccessException e) {
|
||||||
|
@ -191,23 +171,21 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
static class FieldBinding<T> {
|
static class FieldBinding<T> {
|
||||||
final String name;
|
private final Field field;
|
||||||
final Field field;
|
private final JsonAdapter<T> adapter;
|
||||||
final JsonAdapter<T> adapter;
|
|
||||||
|
|
||||||
FieldBinding(String name, Field field, JsonAdapter<T> adapter) {
|
public FieldBinding(Field field, JsonAdapter<T> adapter) {
|
||||||
this.name = name;
|
|
||||||
this.field = field;
|
this.field = field;
|
||||||
this.adapter = adapter;
|
this.adapter = adapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
void read(JsonReader reader, Object value) throws IOException, IllegalAccessException {
|
private void read(JsonReader reader, Object value) throws IOException, IllegalAccessException {
|
||||||
T fieldValue = adapter.fromJson(reader);
|
T fieldValue = adapter.fromJson(reader);
|
||||||
field.set(value, fieldValue);
|
field.set(value, fieldValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked") // We require that field's values are of type T.
|
@SuppressWarnings("unchecked") // We require that field's values are of type T.
|
||||||
void write(JsonWriter writer, Object value) throws IllegalAccessException, IOException {
|
private void write(JsonWriter writer, Object value) throws IllegalAccessException, IOException {
|
||||||
T fieldValue = (T) field.get(value);
|
T fieldValue = (T) field.get(value);
|
||||||
adapter.toJson(writer, fieldValue);
|
adapter.toJson(writer, fieldValue);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,12 +23,11 @@ import java.util.Collection;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import javax.annotation.Nullable;
|
|
||||||
|
|
||||||
/** Converts collection types to JSON arrays containing their converted contents. */
|
/** Converts collection types to JSON arrays containing their converted contents. */
|
||||||
abstract class CollectionJsonAdapter<C extends Collection<T>, T> extends JsonAdapter<C> {
|
abstract class CollectionJsonAdapter<C extends Collection<T>, T> extends JsonAdapter<C> {
|
||||||
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
|
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
|
||||||
@Override public @Nullable JsonAdapter<?> create(
|
@Override public JsonAdapter<?> create(
|
||||||
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||||
Class<?> rawType = Types.getRawType(type);
|
Class<?> rawType = Types.getRawType(type);
|
||||||
if (!annotations.isEmpty()) return null;
|
if (!annotations.isEmpty()) return null;
|
||||||
|
@ -47,7 +46,7 @@ abstract class CollectionJsonAdapter<C extends Collection<T>, T> extends JsonAda
|
||||||
this.elementAdapter = elementAdapter;
|
this.elementAdapter = elementAdapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
static <T> JsonAdapter<Collection<T>> newArrayListAdapter(Type type, Moshi moshi) {
|
private static <T> JsonAdapter<Collection<T>> newArrayListAdapter(Type type, Moshi moshi) {
|
||||||
Type elementType = Types.collectionElementType(type, Collection.class);
|
Type elementType = Types.collectionElementType(type, Collection.class);
|
||||||
JsonAdapter<T> elementAdapter = moshi.adapter(elementType);
|
JsonAdapter<T> elementAdapter = moshi.adapter(elementType);
|
||||||
return new CollectionJsonAdapter<Collection<T>, T>(elementAdapter) {
|
return new CollectionJsonAdapter<Collection<T>, T>(elementAdapter) {
|
||||||
|
@ -57,7 +56,7 @@ abstract class CollectionJsonAdapter<C extends Collection<T>, T> extends JsonAda
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static <T> JsonAdapter<Set<T>> newLinkedHashSetAdapter(Type type, Moshi moshi) {
|
private static <T> JsonAdapter<Set<T>> newLinkedHashSetAdapter(Type type, Moshi moshi) {
|
||||||
Type elementType = Types.collectionElementType(type, Collection.class);
|
Type elementType = Types.collectionElementType(type, Collection.class);
|
||||||
JsonAdapter<T> elementAdapter = moshi.adapter(elementType);
|
JsonAdapter<T> elementAdapter = moshi.adapter(elementType);
|
||||||
return new CollectionJsonAdapter<Set<T>, T>(elementAdapter) {
|
return new CollectionJsonAdapter<Set<T>, T>(elementAdapter) {
|
||||||
|
|
|
@ -19,23 +19,12 @@ import java.lang.annotation.Documented;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.Target;
|
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;
|
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||||
|
|
||||||
/**
|
/** Customizes how a field is encoded as JSON. */
|
||||||
* Customizes how a field is encoded as JSON.
|
@Target({FIELD, METHOD})
|
||||||
*
|
|
||||||
* <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)
|
@Retention(RUNTIME)
|
||||||
@Documented
|
@Documented
|
||||||
public @interface Json {
|
public @interface Json {
|
||||||
|
|
|
@ -15,14 +15,10 @@
|
||||||
*/
|
*/
|
||||||
package com.squareup.moshi;
|
package com.squareup.moshi;
|
||||||
|
|
||||||
import com.squareup.moshi.internal.NullSafeJsonAdapter;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import javax.annotation.CheckReturnValue;
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import okio.Buffer;
|
import okio.Buffer;
|
||||||
import okio.BufferedSink;
|
import okio.BufferedSink;
|
||||||
import okio.BufferedSource;
|
import okio.BufferedSource;
|
||||||
|
@ -31,29 +27,24 @@ import okio.BufferedSource;
|
||||||
* Converts Java values to JSON, and JSON values to Java.
|
* Converts Java values to JSON, and JSON values to Java.
|
||||||
*/
|
*/
|
||||||
public abstract class JsonAdapter<T> {
|
public abstract class JsonAdapter<T> {
|
||||||
@CheckReturnValue public abstract @Nullable T fromJson(JsonReader reader) throws IOException;
|
public abstract T fromJson(JsonReader reader) throws IOException;
|
||||||
|
|
||||||
@CheckReturnValue public final @Nullable T fromJson(BufferedSource source) throws IOException {
|
public final T fromJson(BufferedSource source) throws IOException {
|
||||||
return fromJson(JsonReader.of(source));
|
return fromJson(JsonReader.of(source));
|
||||||
}
|
}
|
||||||
|
|
||||||
@CheckReturnValue public final @Nullable T fromJson(String string) throws IOException {
|
public final T fromJson(String string) throws IOException {
|
||||||
JsonReader reader = JsonReader.of(new Buffer().writeUtf8(string));
|
return fromJson(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, @Nullable T value) throws IOException;
|
public abstract void toJson(JsonWriter writer, T value) throws IOException;
|
||||||
|
|
||||||
public final void toJson(BufferedSink sink, @Nullable T value) throws IOException {
|
public final void toJson(BufferedSink sink, T value) throws IOException {
|
||||||
JsonWriter writer = JsonWriter.of(sink);
|
JsonWriter writer = JsonWriter.of(sink);
|
||||||
toJson(writer, value);
|
toJson(writer, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@CheckReturnValue public final String toJson(@Nullable T value) {
|
public final String toJson(T value) {
|
||||||
Buffer buffer = new Buffer();
|
Buffer buffer = new Buffer();
|
||||||
try {
|
try {
|
||||||
toJson(buffer, value);
|
toJson(buffer, value);
|
||||||
|
@ -63,113 +54,38 @@ public abstract class JsonAdapter<T> {
|
||||||
return buffer.readUtf8();
|
return buffer.readUtf8();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@CheckReturnValue public final @Nullable Object toJsonValue(@Nullable T value) {
|
|
||||||
JsonValueWriter writer = new JsonValueWriter();
|
|
||||||
try {
|
|
||||||
toJson(writer, value);
|
|
||||||
return writer.root();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new AssertionError(e); // No I/O writing to an object.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes a Java value object from {@code value}, which must be comprised of maps, lists,
|
|
||||||
* strings, numbers, booleans and nulls.
|
|
||||||
*/
|
|
||||||
@CheckReturnValue public final @Nullable T fromJsonValue(@Nullable Object value) {
|
|
||||||
JsonValueReader reader = new JsonValueReader(value);
|
|
||||||
try {
|
|
||||||
return fromJson(reader);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new AssertionError(e); // No I/O reading from an object.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a JSON adapter equal to this JSON adapter, but that serializes nulls when encoding
|
|
||||||
* JSON.
|
|
||||||
*/
|
|
||||||
@CheckReturnValue public final JsonAdapter<T> serializeNulls() {
|
|
||||||
final JsonAdapter<T> delegate = this;
|
|
||||||
return new JsonAdapter<T>() {
|
|
||||||
@Override public @Nullable T fromJson(JsonReader reader) throws IOException {
|
|
||||||
return delegate.fromJson(reader);
|
|
||||||
}
|
|
||||||
@Override public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
|
|
||||||
boolean serializeNulls = writer.getSerializeNulls();
|
|
||||||
writer.setSerializeNulls(true);
|
|
||||||
try {
|
|
||||||
delegate.toJson(writer, value);
|
|
||||||
} finally {
|
|
||||||
writer.setSerializeNulls(serializeNulls);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@Override boolean isLenient() {
|
|
||||||
return delegate.isLenient();
|
|
||||||
}
|
|
||||||
@Override public String toString() {
|
|
||||||
return delegate + ".serializeNulls()";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a JSON adapter equal to this JSON adapter, but with support for reading and writing
|
* Returns a JSON adapter equal to this JSON adapter, but with support for reading and writing
|
||||||
* nulls.
|
* nulls.
|
||||||
*/
|
*/
|
||||||
@CheckReturnValue public final JsonAdapter<T> nullSafe() {
|
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;
|
final JsonAdapter<T> delegate = this;
|
||||||
return new JsonAdapter<T>() {
|
return new JsonAdapter<T>() {
|
||||||
@Override public @Nullable T fromJson(JsonReader reader) throws IOException {
|
@Override public T fromJson(JsonReader reader) throws IOException {
|
||||||
if (reader.peek() == JsonReader.Token.NULL) {
|
if (reader.peek() == JsonReader.Token.NULL) {
|
||||||
throw new JsonDataException("Unexpected null at " + reader.getPath());
|
return reader.nextNull();
|
||||||
} else {
|
} else {
|
||||||
return delegate.fromJson(reader);
|
return delegate.fromJson(reader);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Override public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
|
@Override public void toJson(JsonWriter writer, T value) throws IOException {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
throw new JsonDataException("Unexpected null at " + writer.getPath());
|
writer.nullValue();
|
||||||
} else {
|
} else {
|
||||||
delegate.toJson(writer, value);
|
delegate.toJson(writer, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Override boolean isLenient() {
|
|
||||||
return delegate.isLenient();
|
|
||||||
}
|
|
||||||
@Override public String toString() {
|
@Override public String toString() {
|
||||||
return delegate + ".nonNull()";
|
return delegate + ".nullSafe()";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns a JSON adapter equal to this, but is lenient when reading and writing. */
|
/** Returns a JSON adapter equal to this, but is lenient when reading and writing. */
|
||||||
@CheckReturnValue public final JsonAdapter<T> lenient() {
|
public final JsonAdapter<T> lenient() {
|
||||||
final JsonAdapter<T> delegate = this;
|
final JsonAdapter<T> delegate = this;
|
||||||
return new JsonAdapter<T>() {
|
return new JsonAdapter<T>() {
|
||||||
@Override public @Nullable T fromJson(JsonReader reader) throws IOException {
|
@Override public T fromJson(JsonReader reader) throws IOException {
|
||||||
boolean lenient = reader.isLenient();
|
boolean lenient = reader.isLenient();
|
||||||
reader.setLenient(true);
|
reader.setLenient(true);
|
||||||
try {
|
try {
|
||||||
|
@ -178,7 +94,7 @@ public abstract class JsonAdapter<T> {
|
||||||
reader.setLenient(lenient);
|
reader.setLenient(lenient);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Override public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
|
@Override public void toJson(JsonWriter writer, T value) throws IOException {
|
||||||
boolean lenient = writer.isLenient();
|
boolean lenient = writer.isLenient();
|
||||||
writer.setLenient(true);
|
writer.setLenient(true);
|
||||||
try {
|
try {
|
||||||
|
@ -187,9 +103,6 @@ public abstract class JsonAdapter<T> {
|
||||||
writer.setLenient(lenient);
|
writer.setLenient(lenient);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Override boolean isLenient() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
@Override public String toString() {
|
@Override public String toString() {
|
||||||
return delegate + ".lenient()";
|
return delegate + ".lenient()";
|
||||||
}
|
}
|
||||||
|
@ -198,14 +111,14 @@ public abstract class JsonAdapter<T> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a JSON adapter equal to this, but that throws a {@link JsonDataException} when
|
* Returns a JSON adapter equal to this, but that throws a {@link JsonDataException} when
|
||||||
* {@linkplain JsonReader#setFailOnUnknown(boolean) unknown names and values} are encountered.
|
* {@linkplain JsonReader#setFailOnUnknown(boolean) unknown values} are encountered. This
|
||||||
* This constraint applies to both the top-level message handled by this type adapter as well as
|
* constraint applies to both the top-level message handled by this type adapter as well as to
|
||||||
* to nested messages.
|
* nested messages.
|
||||||
*/
|
*/
|
||||||
@CheckReturnValue public final JsonAdapter<T> failOnUnknown() {
|
public final JsonAdapter<T> failOnUnknown() {
|
||||||
final JsonAdapter<T> delegate = this;
|
final JsonAdapter<T> delegate = this;
|
||||||
return new JsonAdapter<T>() {
|
return new JsonAdapter<T>() {
|
||||||
@Override public @Nullable T fromJson(JsonReader reader) throws IOException {
|
@Override public T fromJson(JsonReader reader) throws IOException {
|
||||||
boolean skipForbidden = reader.failOnUnknown();
|
boolean skipForbidden = reader.failOnUnknown();
|
||||||
reader.setFailOnUnknown(true);
|
reader.setFailOnUnknown(true);
|
||||||
try {
|
try {
|
||||||
|
@ -214,67 +127,24 @@ public abstract class JsonAdapter<T> {
|
||||||
reader.setFailOnUnknown(skipForbidden);
|
reader.setFailOnUnknown(skipForbidden);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Override public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
|
@Override public void toJson(JsonWriter writer, T value) throws IOException {
|
||||||
delegate.toJson(writer, value);
|
delegate.toJson(writer, value);
|
||||||
}
|
}
|
||||||
@Override boolean isLenient() {
|
|
||||||
return delegate.isLenient();
|
|
||||||
}
|
|
||||||
@Override public String toString() {
|
@Override public String toString() {
|
||||||
return delegate + ".failOnUnknown()";
|
return delegate + ".failOnUnknown()";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a JSON adapter equal to this, but using {@code indent} to control how the result is
|
|
||||||
* formatted. The {@code indent} string to be repeated for each level of indentation in the
|
|
||||||
* encoded document. If {@code indent.isEmpty()} the encoded document will be compact. Otherwise
|
|
||||||
* the encoded document will be more human-readable.
|
|
||||||
*
|
|
||||||
* @param indent a string containing only whitespace.
|
|
||||||
*/
|
|
||||||
@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 @Nullable T fromJson(JsonReader reader) throws IOException {
|
|
||||||
return delegate.fromJson(reader);
|
|
||||||
}
|
|
||||||
@Override public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
|
|
||||||
String originalIndent = writer.getIndent();
|
|
||||||
writer.setIndent(indent);
|
|
||||||
try {
|
|
||||||
delegate.toJson(writer, value);
|
|
||||||
} finally {
|
|
||||||
writer.setIndent(originalIndent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@Override boolean isLenient() {
|
|
||||||
return delegate.isLenient();
|
|
||||||
}
|
|
||||||
@Override public String toString() {
|
|
||||||
return delegate + ".indent(\"" + indent + "\")";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isLenient() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface Factory {
|
public interface Factory {
|
||||||
/**
|
/**
|
||||||
* Attempts to create an adapter for {@code type} annotated with {@code annotations}. This
|
* 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
|
* returns the adapter if one was created, or null if this factory isn't capable of creating
|
||||||
* such an adapter.
|
* such an adapter.
|
||||||
*
|
*
|
||||||
* <p>Implementations may use {@link Moshi#adapter} to compose adapters of other types, or
|
* <p>Implementations may use to {@link Moshi#adapter} to compose adapters of other types, or
|
||||||
* {@link Moshi#nextAdapter} to delegate to the underlying adapter of the same type.
|
* {@link Moshi#nextAdapter} to delegate to the underlying adapter of the same type.
|
||||||
*/
|
*/
|
||||||
@CheckReturnValue
|
JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi);
|
||||||
@Nullable JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,81 +0,0 @@
|
||||||
/*
|
|
||||||
* 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,8 +15,6 @@
|
||||||
*/
|
*/
|
||||||
package com.squareup.moshi;
|
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
|
* 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
|
* example, suppose the application expects a boolean but the JSON document contains a string. When
|
||||||
|
@ -24,24 +22,20 @@ import javax.annotation.Nullable;
|
||||||
*
|
*
|
||||||
* <p>Exceptions of this type should be fixed by either changing the application code to accept
|
* <p>Exceptions of this type should be fixed by either changing the application code to accept
|
||||||
* the unexpected JSON, or by changing the JSON to conform to the application's expectations.
|
* the unexpected JSON, or by changing the JSON to conform to the application's expectations.
|
||||||
*
|
|
||||||
* <p>This exception may also be triggered if a document's nesting exceeds 31 levels. This depth is
|
|
||||||
* sufficient for all practical applications, but shallow enough to avoid uglier failures like
|
|
||||||
* {@link StackOverflowError}.
|
|
||||||
*/
|
*/
|
||||||
public final class JsonDataException extends RuntimeException {
|
public final class JsonDataException extends RuntimeException {
|
||||||
public JsonDataException() {
|
public JsonDataException() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public JsonDataException(@Nullable String message) {
|
public JsonDataException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public JsonDataException(@Nullable Throwable cause) {
|
public JsonDataException(Throwable cause) {
|
||||||
super(cause);
|
super(cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
public JsonDataException(@Nullable String message, @Nullable Throwable cause) {
|
public JsonDataException(String message, Throwable cause) {
|
||||||
super(message, cause);
|
super(message, cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2016 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.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(@Nullable String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
|
@ -17,8 +17,6 @@ package com.squareup.moshi;
|
||||||
|
|
||||||
/** Lexical scoping elements within a JSON reader or writer. */
|
/** Lexical scoping elements within a JSON reader or writer. */
|
||||||
final class JsonScope {
|
final class JsonScope {
|
||||||
private JsonScope() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/** An array with no elements requires no separators or newlines before it is closed. */
|
/** An array with no elements requires no separators or newlines before it is closed. */
|
||||||
static final int EMPTY_ARRAY = 1;
|
static final int EMPTY_ARRAY = 1;
|
||||||
|
@ -54,7 +52,7 @@ final class JsonScope {
|
||||||
*/
|
*/
|
||||||
static String getPath(int stackSize, int[] stack, String[] pathNames, int[] pathIndices) {
|
static String getPath(int stackSize, int[] stack, String[] pathNames, int[] pathIndices) {
|
||||||
StringBuilder result = new StringBuilder().append('$');
|
StringBuilder result = new StringBuilder().append('$');
|
||||||
for (int i = 0; i < stackSize; i++) {
|
for (int i = 0, size = stackSize; i < size; i++) {
|
||||||
switch (stack[i]) {
|
switch (stack[i]) {
|
||||||
case EMPTY_ARRAY:
|
case EMPTY_ARRAY:
|
||||||
case NONEMPTY_ARRAY:
|
case NONEMPTY_ARRAY:
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,413 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2010 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;
|
|
||||||
|
|
||||||
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;
|
|
||||||
import static com.squareup.moshi.JsonScope.EMPTY_ARRAY;
|
|
||||||
import static com.squareup.moshi.JsonScope.EMPTY_DOCUMENT;
|
|
||||||
import static com.squareup.moshi.JsonScope.EMPTY_OBJECT;
|
|
||||||
import static com.squareup.moshi.JsonScope.NONEMPTY_ARRAY;
|
|
||||||
import static com.squareup.moshi.JsonScope.NONEMPTY_DOCUMENT;
|
|
||||||
import static com.squareup.moshi.JsonScope.NONEMPTY_OBJECT;
|
|
||||||
|
|
||||||
final class JsonUtf8Writer extends JsonWriter {
|
|
||||||
|
|
||||||
/*
|
|
||||||
* From RFC 7159, "All Unicode characters may be placed within the
|
|
||||||
* quotation marks except for the characters that must be escaped:
|
|
||||||
* quotation mark, reverse solidus, and the control characters
|
|
||||||
* (U+0000 through U+001F)."
|
|
||||||
*
|
|
||||||
* We also escape '\u2028' and '\u2029', which JavaScript interprets as
|
|
||||||
* newline characters. This prevents eval() from failing with a syntax
|
|
||||||
* error. http://code.google.com/p/google-gson/issues/detail?id=341
|
|
||||||
*/
|
|
||||||
private static final String[] REPLACEMENT_CHARS;
|
|
||||||
static {
|
|
||||||
REPLACEMENT_CHARS = new String[128];
|
|
||||||
for (int i = 0; i <= 0x1f; i++) {
|
|
||||||
REPLACEMENT_CHARS[i] = String.format("\\u%04x", (int) i);
|
|
||||||
}
|
|
||||||
REPLACEMENT_CHARS['"'] = "\\\"";
|
|
||||||
REPLACEMENT_CHARS['\\'] = "\\\\";
|
|
||||||
REPLACEMENT_CHARS['\t'] = "\\t";
|
|
||||||
REPLACEMENT_CHARS['\b'] = "\\b";
|
|
||||||
REPLACEMENT_CHARS['\n'] = "\\n";
|
|
||||||
REPLACEMENT_CHARS['\r'] = "\\r";
|
|
||||||
REPLACEMENT_CHARS['\f'] = "\\f";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The output data, containing at most one top-level array or object. */
|
|
||||||
private final BufferedSink sink;
|
|
||||||
|
|
||||||
/** The name/value separator; either ":" or ": ". */
|
|
||||||
private String separator = ":";
|
|
||||||
|
|
||||||
private String deferredName;
|
|
||||||
|
|
||||||
JsonUtf8Writer(BufferedSink sink) {
|
|
||||||
if (sink == null) {
|
|
||||||
throw new NullPointerException("sink == null");
|
|
||||||
}
|
|
||||||
this.sink = sink;
|
|
||||||
pushScope(EMPTY_DOCUMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void setIndent(String indent) {
|
|
||||||
super.setIndent(indent);
|
|
||||||
this.separator = !indent.isEmpty() ? ": " : ":";
|
|
||||||
}
|
|
||||||
|
|
||||||
@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, NONEMPTY_ARRAY, "[");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public JsonWriter endArray() throws IOException {
|
|
||||||
return close(EMPTY_ARRAY, NONEMPTY_ARRAY, "]");
|
|
||||||
}
|
|
||||||
|
|
||||||
@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, NONEMPTY_OBJECT, "{");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public JsonWriter endObject() throws IOException {
|
|
||||||
promoteValueToName = false;
|
|
||||||
return close(EMPTY_OBJECT, NONEMPTY_OBJECT, "}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enters a new scope by appending any necessary whitespace and the given
|
|
||||||
* bracket.
|
|
||||||
*/
|
|
||||||
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);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes the current scope by appending any necessary whitespace and the
|
|
||||||
* given bracket.
|
|
||||||
*/
|
|
||||||
private JsonWriter close(int empty, int nonempty, String closeBracket) throws IOException {
|
|
||||||
int context = peekScope();
|
|
||||||
if (context != nonempty && context != empty) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
stackSize--;
|
|
||||||
pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected!
|
|
||||||
pathIndices[stackSize - 1]++;
|
|
||||||
if (context == nonempty) {
|
|
||||||
newline();
|
|
||||||
}
|
|
||||||
sink.writeUtf8(closeBracket);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public JsonWriter name(String name) throws IOException {
|
|
||||||
if (name == null) {
|
|
||||||
throw new NullPointerException("name == null");
|
|
||||||
}
|
|
||||||
if (stackSize == 0) {
|
|
||||||
throw new IllegalStateException("JsonWriter is closed.");
|
|
||||||
}
|
|
||||||
int context = peekScope();
|
|
||||||
if ((context != EMPTY_OBJECT && context != NONEMPTY_OBJECT) || deferredName != null) {
|
|
||||||
throw new IllegalStateException("Nesting problem.");
|
|
||||||
}
|
|
||||||
deferredName = name;
|
|
||||||
pathNames[stackSize - 1] = name;
|
|
||||||
promoteValueToName = false;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeDeferredName() throws IOException {
|
|
||||||
if (deferredName != null) {
|
|
||||||
beforeName();
|
|
||||||
string(sink, deferredName);
|
|
||||||
deferredName = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public JsonWriter value(String value) throws IOException {
|
|
||||||
if (value == null) {
|
|
||||||
return nullValue();
|
|
||||||
}
|
|
||||||
if (promoteValueToName) {
|
|
||||||
return name(value);
|
|
||||||
}
|
|
||||||
writeDeferredName();
|
|
||||||
beforeValue();
|
|
||||||
string(sink, value);
|
|
||||||
pathIndices[stackSize - 1]++;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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();
|
|
||||||
} else {
|
|
||||||
deferredName = null;
|
|
||||||
return this; // skip the name and the value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
beforeValue();
|
|
||||||
sink.writeUtf8("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());
|
|
||||||
}
|
|
||||||
writeDeferredName();
|
|
||||||
beforeValue();
|
|
||||||
sink.writeUtf8(value ? "true" : "false");
|
|
||||||
pathIndices[stackSize - 1]++;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public JsonWriter value(Boolean value) throws IOException {
|
|
||||||
if (value == null) {
|
|
||||||
return nullValue();
|
|
||||||
}
|
|
||||||
return value(value.booleanValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public JsonWriter value(double value) throws IOException {
|
|
||||||
if (!lenient && (Double.isNaN(value) || Double.isInfinite(value))) {
|
|
||||||
throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
|
|
||||||
}
|
|
||||||
if (promoteValueToName) {
|
|
||||||
return name(Double.toString(value));
|
|
||||||
}
|
|
||||||
writeDeferredName();
|
|
||||||
beforeValue();
|
|
||||||
sink.writeUtf8(Double.toString(value));
|
|
||||||
pathIndices[stackSize - 1]++;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public JsonWriter value(long value) throws IOException {
|
|
||||||
if (promoteValueToName) {
|
|
||||||
return name(Long.toString(value));
|
|
||||||
}
|
|
||||||
writeDeferredName();
|
|
||||||
beforeValue();
|
|
||||||
sink.writeUtf8(Long.toString(value));
|
|
||||||
pathIndices[stackSize - 1]++;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public JsonWriter value(@Nullable Number value) throws IOException {
|
|
||||||
if (value == null) {
|
|
||||||
return nullValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
String string = value.toString();
|
|
||||||
if (!lenient
|
|
||||||
&& (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN"))) {
|
|
||||||
throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
|
|
||||||
}
|
|
||||||
if (promoteValueToName) {
|
|
||||||
return name(string);
|
|
||||||
}
|
|
||||||
writeDeferredName();
|
|
||||||
beforeValue();
|
|
||||||
sink.writeUtf8(string);
|
|
||||||
pathIndices[stackSize - 1]++;
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
@Override public void flush() throws IOException {
|
|
||||||
if (stackSize == 0) {
|
|
||||||
throw new IllegalStateException("JsonWriter is closed.");
|
|
||||||
}
|
|
||||||
sink.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flushes and closes this writer and the underlying {@link Sink}.
|
|
||||||
*
|
|
||||||
* @throws JsonDataException if the JSON document is incomplete.
|
|
||||||
*/
|
|
||||||
@Override public void close() throws IOException {
|
|
||||||
sink.close();
|
|
||||||
|
|
||||||
int size = stackSize;
|
|
||||||
if (size > 1 || size == 1 && scopes[size - 1] != NONEMPTY_DOCUMENT) {
|
|
||||||
throw new IOException("Incomplete document");
|
|
||||||
}
|
|
||||||
stackSize = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes {@code value} as a string literal to {@code sink}. This wraps the value in double quotes
|
|
||||||
* and escapes those characters that require it.
|
|
||||||
*/
|
|
||||||
static void string(BufferedSink sink, String value) throws IOException {
|
|
||||||
String[] replacements = REPLACEMENT_CHARS;
|
|
||||||
sink.writeByte('"');
|
|
||||||
int last = 0;
|
|
||||||
int length = value.length();
|
|
||||||
for (int i = 0; i < length; i++) {
|
|
||||||
char c = value.charAt(i);
|
|
||||||
String replacement;
|
|
||||||
if (c < 128) {
|
|
||||||
replacement = replacements[c];
|
|
||||||
if (replacement == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else if (c == '\u2028') {
|
|
||||||
replacement = "\\u2028";
|
|
||||||
} else if (c == '\u2029') {
|
|
||||||
replacement = "\\u2029";
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (last < i) {
|
|
||||||
sink.writeUtf8(value, last, i);
|
|
||||||
}
|
|
||||||
sink.writeUtf8(replacement);
|
|
||||||
last = i + 1;
|
|
||||||
}
|
|
||||||
if (last < length) {
|
|
||||||
sink.writeUtf8(value, last, length);
|
|
||||||
}
|
|
||||||
sink.writeByte('"');
|
|
||||||
}
|
|
||||||
|
|
||||||
private void newline() throws IOException {
|
|
||||||
if (indent == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sink.writeByte('\n');
|
|
||||||
for (int i = 1, size = stackSize; i < size; i++) {
|
|
||||||
sink.writeUtf8(indent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts any necessary separators and whitespace before a name. Also
|
|
||||||
* adjusts the stack to expect the name's value.
|
|
||||||
*/
|
|
||||||
private void beforeName() throws IOException {
|
|
||||||
int context = peekScope();
|
|
||||||
if (context == NONEMPTY_OBJECT) { // first in object
|
|
||||||
sink.writeByte(',');
|
|
||||||
} else if (context != EMPTY_OBJECT) { // not in an object!
|
|
||||||
throw new IllegalStateException("Nesting problem.");
|
|
||||||
}
|
|
||||||
newline();
|
|
||||||
replaceTop(DANGLING_NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts any necessary separators and whitespace before a literal value,
|
|
||||||
* inline array, or inline object. Also adjusts the stack to expect either a
|
|
||||||
* closing bracket or another element.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("fallthrough")
|
|
||||||
private void beforeValue() throws IOException {
|
|
||||||
switch (peekScope()) {
|
|
||||||
case NONEMPTY_DOCUMENT:
|
|
||||||
if (!lenient) {
|
|
||||||
throw new IllegalStateException(
|
|
||||||
"JSON must have only one top-level value.");
|
|
||||||
}
|
|
||||||
// fall-through
|
|
||||||
case EMPTY_DOCUMENT: // first in document
|
|
||||||
replaceTop(NONEMPTY_DOCUMENT);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case EMPTY_ARRAY: // first in array
|
|
||||||
replaceTop(NONEMPTY_ARRAY);
|
|
||||||
newline();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NONEMPTY_ARRAY: // another in array
|
|
||||||
sink.writeByte(',');
|
|
||||||
newline();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case DANGLING_NAME: // value for name
|
|
||||||
sink.writeUtf8(separator);
|
|
||||||
replaceTop(NONEMPTY_OBJECT);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new IllegalStateException("Nesting problem.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,430 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 java.io.IOException;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
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
|
|
||||||
* primitives. It does depth-first traversal keeping a stack starting with the root object. During
|
|
||||||
* traversal a stack tracks the current position in the document:
|
|
||||||
*
|
|
||||||
* <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 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 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
|
|
||||||
* iterator, the next element of that iterator is pushed.
|
|
||||||
* <li>If the top of the stack is an exhausted iterator, calling {@link #endArray} or {@link
|
|
||||||
* #endObject} will pop it.
|
|
||||||
* </ul>
|
|
||||||
*/
|
|
||||||
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 Object[] stack;
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
// If the iterator isn't empty push its first value onto the stack.
|
|
||||||
if (iterator.hasNext()) {
|
|
||||||
push(iterator.next());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void endArray() throws IOException {
|
|
||||||
JsonIterator peeked = require(JsonIterator.class, Token.END_ARRAY);
|
|
||||||
if (peeked.endToken != Token.END_ARRAY || peeked.hasNext()) {
|
|
||||||
throw typeMismatch(peeked, Token.END_ARRAY);
|
|
||||||
}
|
|
||||||
remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void beginObject() throws IOException {
|
|
||||||
Map<?, ?> peeked = require(Map.class, Token.BEGIN_OBJECT);
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
// If the iterator isn't empty push its first value onto the stack.
|
|
||||||
if (iterator.hasNext()) {
|
|
||||||
push(iterator.next());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void endObject() throws IOException {
|
|
||||||
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;
|
|
||||||
remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public boolean hasNext() throws IOException {
|
|
||||||
if (stackSize == 0) return false;
|
|
||||||
|
|
||||||
Object peeked = stack[stackSize - 1];
|
|
||||||
return !(peeked instanceof Iterator) || ((Iterator) peeked).hasNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public Token peek() throws IOException {
|
|
||||||
if (stackSize == 0) return Token.END_DOCUMENT;
|
|
||||||
|
|
||||||
// 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 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;
|
|
||||||
if (peeked instanceof String) return Token.STRING;
|
|
||||||
if (peeked instanceof Boolean) return Token.BOOLEAN;
|
|
||||||
if (peeked instanceof Number) return Token.NUMBER;
|
|
||||||
if (peeked == null) return Token.NULL;
|
|
||||||
if (peeked == JSON_READER_CLOSED) throw new IllegalStateException("JsonReader is closed");
|
|
||||||
|
|
||||||
throw typeMismatch(peeked, "a JSON value");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public String nextName() throws IOException {
|
|
||||||
Map.Entry<?, ?> peeked = require(Map.Entry.class, Token.NAME);
|
|
||||||
|
|
||||||
// Swap the Map.Entry for its value on the stack and return its key.
|
|
||||||
String result = stringKey(peeked);
|
|
||||||
stack[stackSize - 1] = peeked.getValue();
|
|
||||||
pathNames[stackSize - 2] = result;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public int selectName(Options options) throws IOException {
|
|
||||||
Map.Entry<?, ?> peeked = require(Map.Entry.class, Token.NAME);
|
|
||||||
String name = stringKey(peeked);
|
|
||||||
for (int i = 0, length = options.strings.length; i < length; i++) {
|
|
||||||
// Swap the Map.Entry for its value on the stack and return its key.
|
|
||||||
if (options.strings[i].equals(name)) {
|
|
||||||
stack[stackSize - 1] = peeked.getValue();
|
|
||||||
pathNames[stackSize - 2] = name;
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
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 {
|
|
||||||
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(peekedString)) {
|
|
||||||
remove();
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public boolean nextBoolean() throws IOException {
|
|
||||||
Boolean peeked = require(Boolean.class, Token.BOOLEAN);
|
|
||||||
remove();
|
|
||||||
return peeked;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public @Nullable <T> T nextNull() throws IOException {
|
|
||||||
require(Void.class, Token.NULL);
|
|
||||||
remove();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public double nextDouble() throws IOException {
|
|
||||||
Object peeked = require(Object.class, Token.NUMBER);
|
|
||||||
|
|
||||||
double result;
|
|
||||||
if (peeked instanceof Number) {
|
|
||||||
result = ((Number) peeked).doubleValue();
|
|
||||||
} else if (peeked instanceof String) {
|
|
||||||
try {
|
|
||||||
result = Double.parseDouble((String) peeked);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
throw typeMismatch(peeked, Token.NUMBER);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw typeMismatch(peeked, Token.NUMBER);
|
|
||||||
}
|
|
||||||
if (!lenient && (Double.isNaN(result) || Double.isInfinite(result))) {
|
|
||||||
throw new JsonEncodingException("JSON forbids NaN and infinities: " + result
|
|
||||||
+ " at path " + getPath());
|
|
||||||
}
|
|
||||||
remove();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public long nextLong() throws IOException {
|
|
||||||
Object peeked = require(Object.class, Token.NUMBER);
|
|
||||||
|
|
||||||
long result;
|
|
||||||
if (peeked instanceof Number) {
|
|
||||||
result = ((Number) peeked).longValue();
|
|
||||||
} else if (peeked instanceof String) {
|
|
||||||
try {
|
|
||||||
result = Long.parseLong((String) peeked);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
try {
|
|
||||||
BigDecimal asDecimal = new BigDecimal((String) peeked);
|
|
||||||
result = asDecimal.longValueExact();
|
|
||||||
} catch (NumberFormatException e2) {
|
|
||||||
throw typeMismatch(peeked, Token.NUMBER);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw typeMismatch(peeked, Token.NUMBER);
|
|
||||||
}
|
|
||||||
remove();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public int nextInt() throws IOException {
|
|
||||||
Object peeked = require(Object.class, Token.NUMBER);
|
|
||||||
|
|
||||||
int result;
|
|
||||||
if (peeked instanceof Number) {
|
|
||||||
result = ((Number) peeked).intValue();
|
|
||||||
} else if (peeked instanceof String) {
|
|
||||||
try {
|
|
||||||
result = Integer.parseInt((String) peeked);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
try {
|
|
||||||
BigDecimal asDecimal = new BigDecimal((String) peeked);
|
|
||||||
result = asDecimal.intValueExact();
|
|
||||||
} catch (NumberFormatException e2) {
|
|
||||||
throw typeMismatch(peeked, Token.NUMBER);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw typeMismatch(peeked, Token.NUMBER);
|
|
||||||
}
|
|
||||||
remove();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void skipValue() throws IOException {
|
|
||||||
if (failOnUnknown) {
|
|
||||||
throw new JsonDataException("Cannot skip unexpected " + peek() + " at " + getPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this element is in an object clear out the key.
|
|
||||||
if (stackSize > 1) {
|
|
||||||
pathNames[stackSize - 2] = "null";
|
|
||||||
}
|
|
||||||
|
|
||||||
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];
|
|
||||||
stack[stackSize - 1] = entry.getValue();
|
|
||||||
} 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();
|
|
||||||
push(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void close() throws IOException {
|
|
||||||
Arrays.fill(stack, 0, stackSize, null);
|
|
||||||
stack[0] = JSON_READER_CLOSED;
|
|
||||||
scopes[0] = CLOSED;
|
|
||||||
stackSize = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void push(Object newTop) {
|
|
||||||
if (stackSize == stack.length) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 @Nullable <T> T require(Class<T> type, Token expected) throws IOException {
|
|
||||||
Object peeked = (stackSize != 0 ? stack[stackSize - 1] : null);
|
|
||||||
|
|
||||||
if (type.isInstance(peeked)) {
|
|
||||||
return type.cast(peeked);
|
|
||||||
}
|
|
||||||
if (peeked == null && expected == Token.NULL) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (peeked == JSON_READER_CLOSED) {
|
|
||||||
throw new IllegalStateException("JsonReader is closed");
|
|
||||||
}
|
|
||||||
throw typeMismatch(peeked, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String stringKey(Map.Entry<?, ?> entry) {
|
|
||||||
Object name = entry.getKey();
|
|
||||||
if (name instanceof String) return (String) name;
|
|
||||||
throw typeMismatch(name, Token.NAME);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a value and prepares for the next. If we're iterating a map or list this advances the
|
|
||||||
* iterator.
|
|
||||||
*/
|
|
||||||
private void remove() {
|
|
||||||
stackSize--;
|
|
||||||
stack[stackSize] = null;
|
|
||||||
scopes[stackSize] = 0;
|
|
||||||
|
|
||||||
// If we're iterating an array or an object push its next element on to the stack.
|
|
||||||
if (stackSize > 0) {
|
|
||||||
pathIndices[stackSize - 1]++;
|
|
||||||
|
|
||||||
Object parent = stack[stackSize - 1];
|
|
||||||
if (parent instanceof Iterator && ((Iterator<?>) parent).hasNext()) {
|
|
||||||
push(((Iterator<?>) parent).next());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,293 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 java.io.IOException;
|
|
||||||
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;
|
|
||||||
import static com.squareup.moshi.JsonScope.EMPTY_OBJECT;
|
|
||||||
import static com.squareup.moshi.JsonScope.NONEMPTY_DOCUMENT;
|
|
||||||
import static java.lang.Double.NEGATIVE_INFINITY;
|
|
||||||
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 {
|
|
||||||
Object[] stack = new Object[32];
|
|
||||||
private @Nullable String deferredName;
|
|
||||||
|
|
||||||
JsonValueWriter() {
|
|
||||||
pushScope(EMPTY_DOCUMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Object root() {
|
|
||||||
int size = stackSize;
|
|
||||||
if (size > 1 || size == 1 && scopes[size - 1] != NONEMPTY_DOCUMENT) {
|
|
||||||
throw new IllegalStateException("Incomplete document");
|
|
||||||
}
|
|
||||||
return stack[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public JsonWriter beginArray() throws IOException {
|
|
||||||
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;
|
|
||||||
pathIndices[stackSize] = 0;
|
|
||||||
pushScope(EMPTY_ARRAY);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public JsonWriter endArray() throws IOException {
|
|
||||||
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]++;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public JsonWriter beginObject() throws IOException {
|
|
||||||
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;
|
|
||||||
pushScope(EMPTY_OBJECT);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public JsonWriter endObject() throws IOException {
|
|
||||||
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;
|
|
||||||
pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected!
|
|
||||||
pathIndices[stackSize - 1]++;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public JsonWriter name(String name) throws IOException {
|
|
||||||
if (name == null) {
|
|
||||||
throw new NullPointerException("name == null");
|
|
||||||
}
|
|
||||||
if (stackSize == 0) {
|
|
||||||
throw new IllegalStateException("JsonWriter is closed.");
|
|
||||||
}
|
|
||||||
if (peekScope() != EMPTY_OBJECT || deferredName != null) {
|
|
||||||
throw new IllegalStateException("Nesting problem.");
|
|
||||||
}
|
|
||||||
deferredName = name;
|
|
||||||
pathNames[stackSize - 1] = name;
|
|
||||||
promoteValueToName = false;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public JsonWriter value(@Nullable String value) throws IOException {
|
|
||||||
if (promoteValueToName) {
|
|
||||||
return name(value);
|
|
||||||
}
|
|
||||||
add(value);
|
|
||||||
pathIndices[stackSize - 1]++;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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(@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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public JsonWriter value(double value) throws IOException {
|
|
||||||
if (!lenient
|
|
||||||
&& (Double.isNaN(value) || value == NEGATIVE_INFINITY || value == POSITIVE_INFINITY)) {
|
|
||||||
throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
|
|
||||||
}
|
|
||||||
if (promoteValueToName) {
|
|
||||||
return name(Double.toString(value));
|
|
||||||
}
|
|
||||||
add(value);
|
|
||||||
pathIndices[stackSize - 1]++;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public JsonWriter value(long value) throws IOException {
|
|
||||||
if (promoteValueToName) {
|
|
||||||
return name(Long.toString(value));
|
|
||||||
}
|
|
||||||
add(value);
|
|
||||||
pathIndices[stackSize - 1]++;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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
|
|
||||||
|| value instanceof Integer
|
|
||||||
|| value instanceof Long) {
|
|
||||||
return value(value.longValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's trivially converted to a double, do that.
|
|
||||||
if (value instanceof Float || value instanceof Double) {
|
|
||||||
return value(value.doubleValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value == null) {
|
|
||||||
return nullValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Everything else gets converted to a BigDecimal.
|
|
||||||
BigDecimal bigDecimalValue = value instanceof BigDecimal
|
|
||||||
? ((BigDecimal) value)
|
|
||||||
: new BigDecimal(value.toString());
|
|
||||||
if (promoteValueToName) {
|
|
||||||
return name(bigDecimalValue.toString());
|
|
||||||
}
|
|
||||||
add(bigDecimalValue);
|
|
||||||
pathIndices[stackSize - 1]++;
|
|
||||||
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) {
|
|
||||||
throw new IOException("Incomplete document");
|
|
||||||
}
|
|
||||||
stackSize = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void flush() throws IOException {
|
|
||||||
if (stackSize == 0) {
|
|
||||||
throw new IllegalStateException("JsonWriter is closed.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private JsonValueWriter add(@Nullable Object newTop) {
|
|
||||||
int scope = peekScope();
|
|
||||||
|
|
||||||
if (stackSize == 1) {
|
|
||||||
if (scope != EMPTY_DOCUMENT) {
|
|
||||||
throw new IllegalStateException("JSON must have only one top-level value.");
|
|
||||||
}
|
|
||||||
scopes[stackSize - 1] = NONEMPTY_DOCUMENT;
|
|
||||||
stack[stackSize - 1] = newTop;
|
|
||||||
|
|
||||||
} else if (scope == EMPTY_OBJECT && deferredName != null) {
|
|
||||||
if (newTop != null || serializeNulls) {
|
|
||||||
@SuppressWarnings("unchecked") // Our maps always have string keys and object values.
|
|
||||||
Map<String, Object> map = (Map<String, Object>) stack[stackSize - 1];
|
|
||||||
Object replaced = map.put(deferredName, newTop);
|
|
||||||
if (replaced != null) {
|
|
||||||
throw new IllegalArgumentException("Map key '" + deferredName
|
|
||||||
+ "' has multiple values at path " + getPath() + ": " + replaced + " and " + newTop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
deferredName = null;
|
|
||||||
|
|
||||||
} else if (scope == EMPTY_ARRAY) {
|
|
||||||
@SuppressWarnings("unchecked") // Our lists always have object values.
|
|
||||||
List<Object> list = (List<Object>) stack[stackSize - 1];
|
|
||||||
list.add(newTop);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
throw new IllegalStateException("Nesting problem.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -18,19 +18,19 @@ package com.squareup.moshi;
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.Flushable;
|
import java.io.Flushable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
|
||||||
import javax.annotation.CheckReturnValue;
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import okio.BufferedSink;
|
import okio.BufferedSink;
|
||||||
import okio.BufferedSource;
|
import okio.Sink;
|
||||||
|
|
||||||
|
import static com.squareup.moshi.JsonScope.DANGLING_NAME;
|
||||||
import static com.squareup.moshi.JsonScope.EMPTY_ARRAY;
|
import static com.squareup.moshi.JsonScope.EMPTY_ARRAY;
|
||||||
|
import static com.squareup.moshi.JsonScope.EMPTY_DOCUMENT;
|
||||||
import static com.squareup.moshi.JsonScope.EMPTY_OBJECT;
|
import static com.squareup.moshi.JsonScope.EMPTY_OBJECT;
|
||||||
import static com.squareup.moshi.JsonScope.NONEMPTY_ARRAY;
|
import static com.squareup.moshi.JsonScope.NONEMPTY_ARRAY;
|
||||||
|
import static com.squareup.moshi.JsonScope.NONEMPTY_DOCUMENT;
|
||||||
import static com.squareup.moshi.JsonScope.NONEMPTY_OBJECT;
|
import static com.squareup.moshi.JsonScope.NONEMPTY_OBJECT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes a JSON (<a href="http://www.ietf.org/rfc/rfc7159.txt">RFC 7159</a>)
|
* Writes a JSON (<a href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>)
|
||||||
* encoded value to a stream, one token at a time. The stream includes both
|
* encoded value to a stream, one token at a time. The stream includes both
|
||||||
* literal values (strings, numbers, booleans and nulls) as well as the begin
|
* literal values (strings, numbers, booleans and nulls) as well as the begin
|
||||||
* and end delimiters of objects and arrays.
|
* and end delimiters of objects and arrays.
|
||||||
|
@ -77,7 +77,7 @@ import static com.squareup.moshi.JsonScope.NONEMPTY_OBJECT;
|
||||||
* This code encodes the above structure: <pre> {@code
|
* This code encodes the above structure: <pre> {@code
|
||||||
* public void writeJsonStream(BufferedSink sink, List<Message> messages) throws IOException {
|
* public void writeJsonStream(BufferedSink sink, List<Message> messages) throws IOException {
|
||||||
* JsonWriter writer = JsonWriter.of(sink);
|
* JsonWriter writer = JsonWriter.of(sink);
|
||||||
* writer.setIndent(" ");
|
* writer.setIndentSpaces(4);
|
||||||
* writeMessagesArray(writer, messages);
|
* writeMessagesArray(writer, messages);
|
||||||
* writer.close();
|
* writer.close();
|
||||||
* }
|
* }
|
||||||
|
@ -124,87 +124,76 @@ import static com.squareup.moshi.JsonScope.NONEMPTY_OBJECT;
|
||||||
* Instances of this class are not thread safe. Calls that would result in a
|
* Instances of this class are not thread safe. Calls that would result in a
|
||||||
* malformed JSON string will fail with an {@link IllegalStateException}.
|
* malformed JSON string will fail with an {@link IllegalStateException}.
|
||||||
*/
|
*/
|
||||||
public abstract class JsonWriter implements Closeable, Flushable {
|
public class JsonWriter implements Closeable, Flushable {
|
||||||
// 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.
|
* From RFC 4627, "All Unicode characters may be placed within the
|
||||||
int stackSize = 0;
|
* quotation marks except for the characters that must be escaped:
|
||||||
int[] scopes = new int[32];
|
* quotation mark, reverse solidus, and the control characters
|
||||||
String[] pathNames = new String[32];
|
* (U+0000 through U+001F)."
|
||||||
int[] pathIndices = new int[32];
|
*
|
||||||
|
* We also escape '\u2028' and '\u2029', which JavaScript interprets as
|
||||||
|
* newline characters. This prevents eval() from failing with a syntax
|
||||||
|
* error. http://code.google.com/p/google-gson/issues/detail?id=341
|
||||||
|
*/
|
||||||
|
private static final String[] REPLACEMENT_CHARS;
|
||||||
|
static {
|
||||||
|
REPLACEMENT_CHARS = new String[128];
|
||||||
|
for (int i = 0; i <= 0x1f; i++) {
|
||||||
|
REPLACEMENT_CHARS[i] = String.format("\\u%04x", (int) i);
|
||||||
|
}
|
||||||
|
REPLACEMENT_CHARS['"'] = "\\\"";
|
||||||
|
REPLACEMENT_CHARS['\\'] = "\\\\";
|
||||||
|
REPLACEMENT_CHARS['\t'] = "\\t";
|
||||||
|
REPLACEMENT_CHARS['\b'] = "\\b";
|
||||||
|
REPLACEMENT_CHARS['\n'] = "\\n";
|
||||||
|
REPLACEMENT_CHARS['\r'] = "\\r";
|
||||||
|
REPLACEMENT_CHARS['\f'] = "\\f";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The output data, containing at most one top-level array or object. */
|
||||||
|
private final BufferedSink sink;
|
||||||
|
|
||||||
|
private int[] stack = new int[32];
|
||||||
|
private int stackSize = 0;
|
||||||
|
{
|
||||||
|
push(EMPTY_DOCUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] pathNames = new String[32];
|
||||||
|
private int[] pathIndices = new int[32];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A string containing a full set of spaces for a single level of indentation, or null for no
|
* A string containing a full set of spaces for a single level of
|
||||||
* pretty printing.
|
* indentation, or null for no pretty printing.
|
||||||
*/
|
*/
|
||||||
String indent;
|
private String indent;
|
||||||
boolean lenient;
|
|
||||||
boolean serializeNulls;
|
|
||||||
boolean promoteValueToName;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controls the deepest stack size that has begin/end pairs flattened:
|
* The name/value separator; either ":" or ": ".
|
||||||
*
|
|
||||||
* <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;
|
private String separator = ":";
|
||||||
|
|
||||||
/** Returns a new instance that writes UTF-8 encoded JSON to {@code sink}. */
|
private boolean lenient;
|
||||||
@CheckReturnValue public static JsonWriter of(BufferedSink sink) {
|
|
||||||
return new JsonUtf8Writer(sink);
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonWriter() {
|
private String deferredName;
|
||||||
// Package-private to control subclasses.
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the scope on the top of the stack. */
|
private boolean serializeNulls;
|
||||||
final int peekScope() {
|
|
||||||
if (stackSize == 0) {
|
private boolean promoteNameToValue;
|
||||||
throw new IllegalStateException("JsonWriter is closed.");
|
|
||||||
|
private JsonWriter(BufferedSink sink) {
|
||||||
|
if (sink == null) {
|
||||||
|
throw new NullPointerException("sink == null");
|
||||||
}
|
}
|
||||||
return scopes[stackSize - 1];
|
this.sink = sink;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Before pushing a value on the stack this confirms that the stack has capacity. */
|
/**
|
||||||
final boolean checkStack() {
|
* Returns a new instance that writes a JSON-encoded stream to {@code sink}.
|
||||||
if (stackSize != scopes.length) return false;
|
*/
|
||||||
|
public static JsonWriter of(BufferedSink sink) {
|
||||||
if (stackSize == 256) {
|
return new JsonWriter(sink);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Replace the value on the top of the stack with the given value. */
|
|
||||||
final void replaceTop(int topOfStack) {
|
|
||||||
scopes[stackSize - 1] = topOfStack;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -215,22 +204,20 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
||||||
*
|
*
|
||||||
* @param indent a string containing only whitespace.
|
* @param indent a string containing only whitespace.
|
||||||
*/
|
*/
|
||||||
public void setIndent(String indent) {
|
public final void setIndent(String indent) {
|
||||||
this.indent = !indent.isEmpty() ? indent : null;
|
if (indent.length() == 0) {
|
||||||
}
|
this.indent = null;
|
||||||
|
this.separator = ":";
|
||||||
/**
|
} else {
|
||||||
* Returns a string containing only whitespace, used for each level of
|
this.indent = indent;
|
||||||
* indentation. If empty, the encoded document will be compact.
|
this.separator = ": ";
|
||||||
*/
|
}
|
||||||
@CheckReturnValue public final String getIndent() {
|
|
||||||
return indent != null ? indent : "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure this writer to relax its syntax rules. By default, this writer
|
* Configure this writer to relax its syntax rules. By default, this writer
|
||||||
* only emits well-formed JSON as specified by <a
|
* only emits well-formed JSON as specified by <a
|
||||||
* href="http://www.ietf.org/rfc/rfc7159.txt">RFC 7159</a>. Setting the writer
|
* href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>. Setting the writer
|
||||||
* to lenient permits the following:
|
* to lenient permits the following:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Top-level values of any type. With strict writing, the top-level
|
* <li>Top-level values of any type. With strict writing, the top-level
|
||||||
|
@ -246,7 +233,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
||||||
/**
|
/**
|
||||||
* Returns true if this writer has relaxed syntax rules.
|
* Returns true if this writer has relaxed syntax rules.
|
||||||
*/
|
*/
|
||||||
@CheckReturnValue public final boolean isLenient() {
|
public boolean isLenient() {
|
||||||
return lenient;
|
return lenient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -262,7 +249,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
||||||
* Returns true if object members are serialized when their value is null.
|
* Returns true if object members are serialized when their value is null.
|
||||||
* This has no impact on array elements. The default is false.
|
* This has no impact on array elements. The default is false.
|
||||||
*/
|
*/
|
||||||
@CheckReturnValue public final boolean getSerializeNulls() {
|
public final boolean getSerializeNulls() {
|
||||||
return serializeNulls;
|
return serializeNulls;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -272,14 +259,19 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
||||||
*
|
*
|
||||||
* @return this writer.
|
* @return this writer.
|
||||||
*/
|
*/
|
||||||
public abstract JsonWriter beginArray() throws IOException;
|
public JsonWriter beginArray() throws IOException {
|
||||||
|
writeDeferredName();
|
||||||
|
return open(EMPTY_ARRAY, "[");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ends encoding the current array.
|
* Ends encoding the current array.
|
||||||
*
|
*
|
||||||
* @return this writer.
|
* @return this writer.
|
||||||
*/
|
*/
|
||||||
public abstract JsonWriter endArray() throws IOException;
|
public JsonWriter endArray() throws IOException {
|
||||||
|
return close(EMPTY_ARRAY, NONEMPTY_ARRAY, "]");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Begins encoding a new object. Each call to this method must be paired
|
* Begins encoding a new object. Each call to this method must be paired
|
||||||
|
@ -287,22 +279,112 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
||||||
*
|
*
|
||||||
* @return this writer.
|
* @return this writer.
|
||||||
*/
|
*/
|
||||||
public abstract JsonWriter beginObject() throws IOException;
|
public JsonWriter beginObject() throws IOException {
|
||||||
|
writeDeferredName();
|
||||||
|
return open(EMPTY_OBJECT, "{");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ends encoding the current object.
|
* Ends encoding the current object.
|
||||||
*
|
*
|
||||||
* @return this writer.
|
* @return this writer.
|
||||||
*/
|
*/
|
||||||
public abstract JsonWriter endObject() throws IOException;
|
public JsonWriter endObject() throws IOException {
|
||||||
|
promoteNameToValue = false;
|
||||||
|
return close(EMPTY_OBJECT, NONEMPTY_OBJECT, "}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enters a new scope by appending any necessary whitespace and the given
|
||||||
|
* bracket.
|
||||||
|
*/
|
||||||
|
private JsonWriter open(int empty, String openBracket) throws IOException {
|
||||||
|
beforeValue(true);
|
||||||
|
pathIndices[stackSize] = 0;
|
||||||
|
push(empty);
|
||||||
|
sink.writeUtf8(openBracket);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the current scope by appending any necessary whitespace and the
|
||||||
|
* given bracket.
|
||||||
|
*/
|
||||||
|
private JsonWriter close(int empty, int nonempty, String closeBracket)
|
||||||
|
throws IOException {
|
||||||
|
int context = peek();
|
||||||
|
if (context != nonempty && context != empty) {
|
||||||
|
throw new IllegalStateException("Nesting problem.");
|
||||||
|
}
|
||||||
|
if (deferredName != null) {
|
||||||
|
throw new IllegalStateException("Dangling name: " + deferredName);
|
||||||
|
}
|
||||||
|
|
||||||
|
stackSize--;
|
||||||
|
pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected!
|
||||||
|
pathIndices[stackSize - 1]++;
|
||||||
|
if (context == nonempty) {
|
||||||
|
newline();
|
||||||
|
}
|
||||||
|
sink.writeUtf8(closeBracket);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void push(int newTop) {
|
||||||
|
if (stackSize == stack.length) {
|
||||||
|
int[] newStack = new int[stackSize * 2];
|
||||||
|
System.arraycopy(stack, 0, newStack, 0, stackSize);
|
||||||
|
stack = newStack;
|
||||||
|
}
|
||||||
|
stack[stackSize++] = newTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value on the top of the stack.
|
||||||
|
*/
|
||||||
|
private int peek() {
|
||||||
|
if (stackSize == 0) {
|
||||||
|
throw new IllegalStateException("JsonWriter is closed.");
|
||||||
|
}
|
||||||
|
return stack[stackSize - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the value on the top of the stack with the given value.
|
||||||
|
*/
|
||||||
|
private void replaceTop(int topOfStack) {
|
||||||
|
stack[stackSize - 1] = topOfStack;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encodes the property name.
|
* Encodes the property name.
|
||||||
*
|
*
|
||||||
* @param name the name of the forthcoming value. Must not be null.
|
* @param name the name of the forthcoming value. May not be null.
|
||||||
* @return this writer.
|
* @return this writer.
|
||||||
*/
|
*/
|
||||||
public abstract JsonWriter name(String name) throws IOException;
|
public JsonWriter name(String name) throws IOException {
|
||||||
|
if (name == null) {
|
||||||
|
throw new NullPointerException("name == null");
|
||||||
|
}
|
||||||
|
if (stackSize == 0) {
|
||||||
|
throw new IllegalStateException("JsonWriter is closed.");
|
||||||
|
}
|
||||||
|
if (deferredName != null) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
deferredName = name;
|
||||||
|
pathNames[stackSize - 1] = name;
|
||||||
|
promoteNameToValue = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeDeferredName() throws IOException {
|
||||||
|
if (deferredName != null) {
|
||||||
|
beforeName();
|
||||||
|
string(deferredName);
|
||||||
|
deferredName = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encodes {@code value}.
|
* Encodes {@code value}.
|
||||||
|
@ -310,28 +392,52 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
||||||
* @param value the literal string value, or null to encode a null literal.
|
* @param value the literal string value, or null to encode a null literal.
|
||||||
* @return this writer.
|
* @return this writer.
|
||||||
*/
|
*/
|
||||||
public abstract JsonWriter value(@Nullable String value) throws IOException;
|
public JsonWriter value(String value) throws IOException {
|
||||||
|
if (value == null) {
|
||||||
|
return nullValue();
|
||||||
|
}
|
||||||
|
if (promoteNameToValue) {
|
||||||
|
return name(value);
|
||||||
|
}
|
||||||
|
writeDeferredName();
|
||||||
|
beforeValue(false);
|
||||||
|
string(value);
|
||||||
|
pathIndices[stackSize - 1]++;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encodes {@code null}.
|
* Encodes {@code null}.
|
||||||
*
|
*
|
||||||
* @return this writer.
|
* @return this writer.
|
||||||
*/
|
*/
|
||||||
public abstract JsonWriter nullValue() throws IOException;
|
public JsonWriter nullValue() throws IOException {
|
||||||
|
if (deferredName != null) {
|
||||||
|
if (serializeNulls) {
|
||||||
|
writeDeferredName();
|
||||||
|
} else {
|
||||||
|
deferredName = null;
|
||||||
|
return this; // skip the name and the value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
beforeValue(false);
|
||||||
|
sink.writeUtf8("null");
|
||||||
|
pathIndices[stackSize - 1]++;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encodes {@code value}.
|
* Encodes {@code value}.
|
||||||
*
|
*
|
||||||
* @return this writer.
|
* @return this writer.
|
||||||
*/
|
*/
|
||||||
public abstract JsonWriter value(boolean value) throws IOException;
|
public JsonWriter value(boolean value) throws IOException {
|
||||||
|
writeDeferredName();
|
||||||
/**
|
beforeValue(false);
|
||||||
* Encodes {@code value}.
|
sink.writeUtf8(value ? "true" : "false");
|
||||||
*
|
pathIndices[stackSize - 1]++;
|
||||||
* @return this writer.
|
return this;
|
||||||
*/
|
}
|
||||||
public abstract JsonWriter value(@Nullable Boolean value) throws IOException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encodes {@code value}.
|
* Encodes {@code value}.
|
||||||
|
@ -340,14 +446,35 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
||||||
* {@linkplain Double#isInfinite() infinities}.
|
* {@linkplain Double#isInfinite() infinities}.
|
||||||
* @return this writer.
|
* @return this writer.
|
||||||
*/
|
*/
|
||||||
public abstract JsonWriter value(double value) throws IOException;
|
public JsonWriter value(double value) throws IOException {
|
||||||
|
if (Double.isNaN(value) || Double.isInfinite(value)) {
|
||||||
|
throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
|
||||||
|
}
|
||||||
|
if (promoteNameToValue) {
|
||||||
|
return name(Double.toString(value));
|
||||||
|
}
|
||||||
|
writeDeferredName();
|
||||||
|
beforeValue(false);
|
||||||
|
sink.writeUtf8(Double.toString(value));
|
||||||
|
pathIndices[stackSize - 1]++;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encodes {@code value}.
|
* Encodes {@code value}.
|
||||||
*
|
*
|
||||||
* @return this writer.
|
* @return this writer.
|
||||||
*/
|
*/
|
||||||
public abstract JsonWriter value(long value) throws IOException;
|
public JsonWriter value(long value) throws IOException {
|
||||||
|
if (promoteNameToValue) {
|
||||||
|
return name(Long.toString(value));
|
||||||
|
}
|
||||||
|
writeDeferredName();
|
||||||
|
beforeValue(false);
|
||||||
|
sink.writeUtf8(Long.toString(value));
|
||||||
|
pathIndices[stackSize - 1]++;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encodes {@code value}.
|
* Encodes {@code value}.
|
||||||
|
@ -356,116 +483,172 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
||||||
* {@linkplain Double#isInfinite() infinities}.
|
* {@linkplain Double#isInfinite() infinities}.
|
||||||
* @return this writer.
|
* @return this writer.
|
||||||
*/
|
*/
|
||||||
public abstract JsonWriter value(@Nullable Number value) throws IOException;
|
public JsonWriter value(Number value) throws IOException {
|
||||||
|
if (value == null) {
|
||||||
|
return nullValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
String string = value.toString();
|
||||||
|
if (!lenient
|
||||||
|
&& (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN"))) {
|
||||||
|
throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
|
||||||
|
}
|
||||||
|
if (promoteNameToValue) {
|
||||||
|
return name(string);
|
||||||
|
}
|
||||||
|
writeDeferredName();
|
||||||
|
beforeValue(false);
|
||||||
|
sink.writeUtf8(string);
|
||||||
|
pathIndices[stackSize - 1]++;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes {@code source} directly without encoding its contents.
|
* Ensures all buffered data is written to the underlying {@link Sink}
|
||||||
* Since no validation is performed, {@link #setSerializeNulls} and other writer configurations
|
* and flushes that writer.
|
||||||
* are not respected.
|
*/
|
||||||
|
public void flush() throws IOException {
|
||||||
|
if (stackSize == 0) {
|
||||||
|
throw new IllegalStateException("JsonWriter is closed.");
|
||||||
|
}
|
||||||
|
sink.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flushes and closes this writer and the underlying {@link Sink}.
|
||||||
*
|
*
|
||||||
* @return this writer.
|
* @throws JsonDataException if the JSON document is incomplete.
|
||||||
*/
|
*/
|
||||||
public abstract JsonWriter value(BufferedSource source) throws IOException;
|
public void close() throws IOException {
|
||||||
|
sink.close();
|
||||||
|
|
||||||
|
int size = stackSize;
|
||||||
|
if (size > 1 || size == 1 && stack[size - 1] != NONEMPTY_DOCUMENT) {
|
||||||
|
throw new IOException("Incomplete document");
|
||||||
|
}
|
||||||
|
stackSize = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void string(String value) throws IOException {
|
||||||
|
String[] replacements = REPLACEMENT_CHARS;
|
||||||
|
sink.writeByte('"');
|
||||||
|
int last = 0;
|
||||||
|
int length = value.length();
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
char c = value.charAt(i);
|
||||||
|
String replacement;
|
||||||
|
if (c < 128) {
|
||||||
|
replacement = replacements[c];
|
||||||
|
if (replacement == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if (c == '\u2028') {
|
||||||
|
replacement = "\\u2028";
|
||||||
|
} else if (c == '\u2029') {
|
||||||
|
replacement = "\\u2029";
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (last < i) {
|
||||||
|
sink.writeUtf8(value, last, i);
|
||||||
|
}
|
||||||
|
sink.writeUtf8(replacement);
|
||||||
|
last = i + 1;
|
||||||
|
}
|
||||||
|
if (last < length) {
|
||||||
|
sink.writeUtf8(value, last, length);
|
||||||
|
}
|
||||||
|
sink.writeByte('"');
|
||||||
|
}
|
||||||
|
|
||||||
|
private void newline() throws IOException {
|
||||||
|
if (indent == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sink.writeByte('\n');
|
||||||
|
for (int i = 1, size = stackSize; i < size; i++) {
|
||||||
|
sink.writeUtf8(indent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes the writer to treat the next value as a string name. This is useful for map adapters so
|
* Inserts any necessary separators and whitespace before a name. Also
|
||||||
* that arbitrary type adapters can use {@link #value} to write a name value.
|
* adjusts the stack to expect the name's value.
|
||||||
*/
|
*/
|
||||||
final void promoteValueToName() throws IOException {
|
private void beforeName() throws IOException {
|
||||||
int context = peekScope();
|
int context = peek();
|
||||||
|
if (context == NONEMPTY_OBJECT) { // first in object
|
||||||
|
sink.writeByte(',');
|
||||||
|
} else if (context != EMPTY_OBJECT) { // not in an object!
|
||||||
|
throw new IllegalStateException("Nesting problem.");
|
||||||
|
}
|
||||||
|
newline();
|
||||||
|
replaceTop(DANGLING_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts any necessary separators and whitespace before a literal value,
|
||||||
|
* inline array, or inline object. Also adjusts the stack to expect either a
|
||||||
|
* closing bracket or another element.
|
||||||
|
*
|
||||||
|
* @param root true if the value is a new array or object, the two values
|
||||||
|
* permitted as top-level elements.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("fallthrough")
|
||||||
|
private void beforeValue(boolean root) throws IOException {
|
||||||
|
switch (peek()) {
|
||||||
|
case NONEMPTY_DOCUMENT:
|
||||||
|
if (!lenient) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"JSON must have only one top-level value.");
|
||||||
|
}
|
||||||
|
// fall-through
|
||||||
|
case EMPTY_DOCUMENT: // first in document
|
||||||
|
if (!lenient && !root) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"JSON must start with an array or an object.");
|
||||||
|
}
|
||||||
|
replaceTop(NONEMPTY_DOCUMENT);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EMPTY_ARRAY: // first in array
|
||||||
|
replaceTop(NONEMPTY_ARRAY);
|
||||||
|
newline();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NONEMPTY_ARRAY: // another in array
|
||||||
|
sink.writeByte(',');
|
||||||
|
newline();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DANGLING_NAME: // value for name
|
||||||
|
sink.writeUtf8(separator);
|
||||||
|
replaceTop(NONEMPTY_OBJECT);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException("Nesting problem.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the reader to treat the next string value as a name. This is useful for map adapters so
|
||||||
|
* that arbitrary type adapters can use {@link #value(String)} to write a name value.
|
||||||
|
*/
|
||||||
|
void promoteNameToValue() throws IOException {
|
||||||
|
int context = peek();
|
||||||
if (context != NONEMPTY_OBJECT && context != EMPTY_OBJECT) {
|
if (context != NONEMPTY_OBJECT && context != EMPTY_OBJECT) {
|
||||||
throw new IllegalStateException("Nesting problem.");
|
throw new IllegalStateException("Nesting problem.");
|
||||||
}
|
}
|
||||||
promoteValueToName = true;
|
promoteNameToValue = 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
|
* Returns a <a href="http://goessner.net/articles/JsonPath/">JsonPath</a> to
|
||||||
* the current location in the JSON value.
|
* the current location in the JSON value.
|
||||||
*/
|
*/
|
||||||
@CheckReturnValue public final String getPath() {
|
public String getPath() {
|
||||||
return JsonScope.getPath(stackSize, scopes, pathNames, pathIndices);
|
return JsonScope.getPath(stackSize, stack, 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
|
* Create a natural order, empty tree map whose keys must be mutually
|
||||||
* comparable and non-null.
|
* comparable and non-null.
|
||||||
*/
|
*/
|
||||||
LinkedHashTreeMap() {
|
public LinkedHashTreeMap() {
|
||||||
this(null);
|
this(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,10 +66,8 @@ final class LinkedHashTreeMap<K, V> extends AbstractMap<K, V> implements Seriali
|
||||||
* @param comparator the comparator to order elements with, or {@code null} to
|
* @param comparator the comparator to order elements with, or {@code null} to
|
||||||
* use the natural ordering.
|
* use the natural ordering.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({
|
@SuppressWarnings({ "unchecked", "rawtypes" }) // unsafe! if comparator is null, this assumes K is comparable
|
||||||
"unchecked", "rawtypes" // Unsafe! if comparator is null, this assumes K is comparable.
|
public LinkedHashTreeMap(Comparator<? super K> comparator) {
|
||||||
})
|
|
||||||
LinkedHashTreeMap(Comparator<? super K> comparator) {
|
|
||||||
this.comparator = comparator != null
|
this.comparator = comparator != null
|
||||||
? comparator
|
? comparator
|
||||||
: (Comparator) NATURAL_ORDER;
|
: (Comparator) NATURAL_ORDER;
|
||||||
|
@ -475,14 +473,14 @@ final class LinkedHashTreeMap<K, V> extends AbstractMap<K, V> implements Seriali
|
||||||
V value;
|
V value;
|
||||||
int height;
|
int height;
|
||||||
|
|
||||||
/** Create the header entry. */
|
/** Create the header entry */
|
||||||
Node() {
|
Node() {
|
||||||
key = null;
|
key = null;
|
||||||
hash = -1;
|
hash = -1;
|
||||||
next = prev = this;
|
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) {
|
Node(Node<K, V> parent, K key, int hash, Node<K, V> next, Node<K, V> prev) {
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
this.key = key;
|
this.key = key;
|
||||||
|
@ -667,7 +665,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
|
* comparisons. Using this class to create a tree of size <i>S</i> is
|
||||||
* {@code O(S)}.
|
* {@code O(S)}.
|
||||||
*/
|
*/
|
||||||
static final class AvlBuilder<K, V> {
|
final static class AvlBuilder<K, V> {
|
||||||
/** This stack is a singly linked list, linked by the 'parent' field. */
|
/** This stack is a singly linked list, linked by the 'parent' field. */
|
||||||
private Node<K, V> stack;
|
private Node<K, V> stack;
|
||||||
private int leavesToSkip;
|
private int leavesToSkip;
|
||||||
|
@ -757,7 +755,7 @@ final class LinkedHashTreeMap<K, V> extends AbstractMap<K, V> implements Seriali
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class LinkedTreeMapIterator<T> implements Iterator<T> {
|
private abstract class LinkedTreeMapIterator<T> implements Iterator<T> {
|
||||||
Node<K, V> next = header.next;
|
Node<K, V> next = header.next;
|
||||||
Node<K, V> lastReturned = null;
|
Node<K, V> lastReturned = null;
|
||||||
int expectedModCount = modCount;
|
int expectedModCount = modCount;
|
||||||
|
|
|
@ -20,7 +20,6 @@ import java.lang.annotation.Annotation;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import javax.annotation.Nullable;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts maps with string keys to JSON objects.
|
* Converts maps with string keys to JSON objects.
|
||||||
|
@ -29,7 +28,7 @@ import javax.annotation.Nullable;
|
||||||
*/
|
*/
|
||||||
final class MapJsonAdapter<K, V> extends JsonAdapter<Map<K, V>> {
|
final class MapJsonAdapter<K, V> extends JsonAdapter<Map<K, V>> {
|
||||||
public static final Factory FACTORY = new Factory() {
|
public static final Factory FACTORY = new Factory() {
|
||||||
@Override public @Nullable JsonAdapter<?> create(
|
@Override public JsonAdapter<?> create(
|
||||||
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||||
if (!annotations.isEmpty()) return null;
|
if (!annotations.isEmpty()) return null;
|
||||||
Class<?> rawType = Types.getRawType(type);
|
Class<?> rawType = Types.getRawType(type);
|
||||||
|
@ -42,7 +41,7 @@ final class MapJsonAdapter<K, V> extends JsonAdapter<Map<K, V>> {
|
||||||
private final JsonAdapter<K> keyAdapter;
|
private final JsonAdapter<K> keyAdapter;
|
||||||
private final JsonAdapter<V> valueAdapter;
|
private final JsonAdapter<V> valueAdapter;
|
||||||
|
|
||||||
MapJsonAdapter(Moshi moshi, Type keyType, Type valueType) {
|
public MapJsonAdapter(Moshi moshi, Type keyType, Type valueType) {
|
||||||
this.keyAdapter = moshi.adapter(keyType);
|
this.keyAdapter = moshi.adapter(keyType);
|
||||||
this.valueAdapter = moshi.adapter(valueType);
|
this.valueAdapter = moshi.adapter(valueType);
|
||||||
}
|
}
|
||||||
|
@ -51,9 +50,9 @@ final class MapJsonAdapter<K, V> extends JsonAdapter<Map<K, V>> {
|
||||||
writer.beginObject();
|
writer.beginObject();
|
||||||
for (Map.Entry<K, V> entry : map.entrySet()) {
|
for (Map.Entry<K, V> entry : map.entrySet()) {
|
||||||
if (entry.getKey() == null) {
|
if (entry.getKey() == null) {
|
||||||
throw new JsonDataException("Map key is null at " + writer.getPath());
|
throw new JsonDataException("Map key is null at path " + writer.getPath());
|
||||||
}
|
}
|
||||||
writer.promoteValueToName();
|
writer.promoteNameToValue();
|
||||||
keyAdapter.toJson(writer, entry.getKey());
|
keyAdapter.toJson(writer, entry.getKey());
|
||||||
valueAdapter.toJson(writer, entry.getValue());
|
valueAdapter.toJson(writer, entry.getValue());
|
||||||
}
|
}
|
||||||
|
@ -70,7 +69,7 @@ final class MapJsonAdapter<K, V> extends JsonAdapter<Map<K, V>> {
|
||||||
V replaced = result.put(name, value);
|
V replaced = result.put(name, value);
|
||||||
if (replaced != null) {
|
if (replaced != null) {
|
||||||
throw new JsonDataException("Map key '" + name + "' has multiple values at path "
|
throw new JsonDataException("Map key '" + name + "' has multiple values at path "
|
||||||
+ reader.getPath() + ": " + replaced + " and " + value);
|
+ reader.getPath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reader.endObject();
|
reader.endObject();
|
||||||
|
|
|
@ -15,106 +15,47 @@
|
||||||
*/
|
*/
|
||||||
package com.squareup.moshi;
|
package com.squareup.moshi;
|
||||||
|
|
||||||
import com.squareup.moshi.internal.Util;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.util.ArrayDeque;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Deque;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
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.
|
* Coordinates binding between JSON values and Java objects.
|
||||||
*/
|
*/
|
||||||
public final class Moshi {
|
public final class Moshi {
|
||||||
static final List<JsonAdapter.Factory> BUILT_IN_FACTORIES = new ArrayList<>(5);
|
|
||||||
|
|
||||||
static {
|
|
||||||
BUILT_IN_FACTORIES.add(StandardJsonAdapters.FACTORY);
|
|
||||||
BUILT_IN_FACTORIES.add(CollectionJsonAdapter.FACTORY);
|
|
||||||
BUILT_IN_FACTORIES.add(MapJsonAdapter.FACTORY);
|
|
||||||
BUILT_IN_FACTORIES.add(ArrayJsonAdapter.FACTORY);
|
|
||||||
BUILT_IN_FACTORIES.add(ClassJsonAdapter.FACTORY);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final List<JsonAdapter.Factory> factories;
|
private final List<JsonAdapter.Factory> factories;
|
||||||
private final ThreadLocal<LookupChain> lookupChainThreadLocal = new ThreadLocal<>();
|
private final ThreadLocal<List<DeferredAdapter<?>>> reentrantCalls = new ThreadLocal<>();
|
||||||
private final Map<Object, JsonAdapter<?>> adapterCache = new LinkedHashMap<>();
|
private final Map<Object, JsonAdapter<?>> adapterCache = new LinkedHashMap<>();
|
||||||
|
|
||||||
Moshi(Builder builder) {
|
private Moshi(Builder builder) {
|
||||||
List<JsonAdapter.Factory> factories = new ArrayList<>(
|
List<JsonAdapter.Factory> factories = new ArrayList<>();
|
||||||
builder.factories.size() + BUILT_IN_FACTORIES.size());
|
|
||||||
factories.addAll(builder.factories);
|
factories.addAll(builder.factories);
|
||||||
factories.addAll(BUILT_IN_FACTORIES);
|
factories.add(StandardJsonAdapters.FACTORY);
|
||||||
|
factories.add(CollectionJsonAdapter.FACTORY);
|
||||||
|
factories.add(MapJsonAdapter.FACTORY);
|
||||||
|
factories.add(ArrayJsonAdapter.FACTORY);
|
||||||
|
factories.add(ClassJsonAdapter.FACTORY);
|
||||||
this.factories = Collections.unmodifiableList(factories);
|
this.factories = Collections.unmodifiableList(factories);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns a JSON adapter for {@code type}, creating it if necessary. */
|
/** Returns a JSON adapter for {@code type}, creating it if necessary. */
|
||||||
@CheckReturnValue public <T> JsonAdapter<T> adapter(Type type) {
|
public <T> JsonAdapter<T> adapter(Type type) {
|
||||||
return adapter(type, Util.NO_ANNOTATIONS);
|
return adapter(type, Util.NO_ANNOTATIONS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@CheckReturnValue public <T> JsonAdapter<T> adapter(Class<T> type) {
|
public <T> JsonAdapter<T> adapter(Class<T> type) {
|
||||||
return adapter(type, Util.NO_ANNOTATIONS);
|
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)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@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) {
|
|
||||||
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.
|
@SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters.
|
||||||
public <T> JsonAdapter<T> adapter(Type type, Set<? extends Annotation> annotations,
|
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!
|
// If there's an equivalent adapter in the cache, we're done!
|
||||||
Object cacheKey = cacheKey(type, annotations);
|
Object cacheKey = cacheKey(type, annotations);
|
||||||
synchronized (adapterCache) {
|
synchronized (adapterCache) {
|
||||||
|
@ -122,45 +63,47 @@ public final class Moshi {
|
||||||
if (result != null) return (JsonAdapter<T>) result;
|
if (result != null) return (JsonAdapter<T>) result;
|
||||||
}
|
}
|
||||||
|
|
||||||
LookupChain lookupChain = lookupChainThreadLocal.get();
|
// Short-circuit if this is a reentrant call.
|
||||||
if (lookupChain == null) {
|
List<DeferredAdapter<?>> deferredAdapters = reentrantCalls.get();
|
||||||
lookupChain = new LookupChain();
|
if (deferredAdapters != null) {
|
||||||
lookupChainThreadLocal.set(lookupChain);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean success = false;
|
// Prepare for re-entrant calls, then ask each factory to create a type adapter.
|
||||||
JsonAdapter<T> adapterFromCall = lookupChain.push(type, fieldName, cacheKey);
|
DeferredAdapter<T> deferredAdapter = new DeferredAdapter<>(cacheKey);
|
||||||
|
deferredAdapters.add(deferredAdapter);
|
||||||
try {
|
try {
|
||||||
if (adapterFromCall != null) return adapterFromCall;
|
|
||||||
|
|
||||||
// Ask each factory to create the JSON adapter.
|
|
||||||
for (int i = 0, size = factories.size(); i < size; i++) {
|
for (int i = 0, size = factories.size(); i < size; i++) {
|
||||||
JsonAdapter<T> result = (JsonAdapter<T>) factories.get(i).create(type, annotations, this);
|
JsonAdapter<T> result = (JsonAdapter<T>) factories.get(i).create(type, annotations, this);
|
||||||
if (result == null) continue;
|
if (result != null) {
|
||||||
|
deferredAdapter.ready(result);
|
||||||
// Success! Notify the LookupChain so it is cached and can be used by re-entrant calls.
|
synchronized (adapterCache) {
|
||||||
lookupChain.adapterFound(result);
|
adapterCache.put(cacheKey, result);
|
||||||
success = true;
|
}
|
||||||
return result;
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"No JsonAdapter for " + typeAnnotatedWithAnnotations(type, annotations));
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
throw lookupChain.exceptionWithLookupStack(e);
|
|
||||||
} finally {
|
} finally {
|
||||||
lookupChain.pop(success);
|
deferredAdapters.remove(deferredAdapters.size() - 1);
|
||||||
|
if (deferredAdapters.isEmpty()) {
|
||||||
|
reentrantCalls.remove();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("No JsonAdapter for " + type + " annotated " + annotations);
|
||||||
}
|
}
|
||||||
|
|
||||||
@CheckReturnValue
|
|
||||||
@SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters.
|
@SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters.
|
||||||
public <T> JsonAdapter<T> nextAdapter(JsonAdapter.Factory skipPast, Type type,
|
public <T> JsonAdapter<T> nextAdapter(JsonAdapter.Factory skipPast, Type type,
|
||||||
Set<? extends Annotation> annotations) {
|
Set<? extends Annotation> annotations) {
|
||||||
if (annotations == null) throw new NullPointerException("annotations == null");
|
|
||||||
|
|
||||||
type = removeSubtypeWildcard(canonicalize(type));
|
|
||||||
|
|
||||||
int skipPastIndex = factories.indexOf(skipPast);
|
int skipPastIndex = factories.indexOf(skipPast);
|
||||||
if (skipPastIndex == -1) {
|
if (skipPastIndex == -1) {
|
||||||
throw new IllegalArgumentException("Unable to skip past unknown factory " + skipPast);
|
throw new IllegalArgumentException("Unable to skip past unknown factory " + skipPast);
|
||||||
|
@ -170,15 +113,7 @@ public final class Moshi {
|
||||||
if (result != null) return result;
|
if (result != null) return result;
|
||||||
}
|
}
|
||||||
throw new IllegalArgumentException("No next JsonAdapter for "
|
throw new IllegalArgumentException("No next JsonAdapter for "
|
||||||
+ typeAnnotatedWithAnnotations(type, annotations));
|
+ type + " annotated " + annotations);
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns a new builder containing all custom factories used by the current instance. */
|
|
||||||
@CheckReturnValue public Moshi.Builder newBuilder() {
|
|
||||||
int fullSize = factories.size();
|
|
||||||
int tailSize = BUILT_IN_FACTORIES.size();
|
|
||||||
List<JsonAdapter.Factory> customFactories = factories.subList(0, fullSize - tailSize);
|
|
||||||
return new Builder().addAll(customFactories);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns an opaque object that's equal if the type and annotations are equal. */
|
/** Returns an opaque object that's equal if the type and annotations are equal. */
|
||||||
|
@ -188,14 +123,14 @@ public final class Moshi {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class Builder {
|
public static final class Builder {
|
||||||
final List<JsonAdapter.Factory> factories = new ArrayList<>();
|
private final List<JsonAdapter.Factory> factories = new ArrayList<>();
|
||||||
|
|
||||||
public <T> Builder add(final Type type, final JsonAdapter<T> jsonAdapter) {
|
public <T> Builder add(final Type type, final JsonAdapter<T> jsonAdapter) {
|
||||||
if (type == null) throw new IllegalArgumentException("type == null");
|
if (type == null) throw new IllegalArgumentException("type == null");
|
||||||
if (jsonAdapter == null) throw new IllegalArgumentException("jsonAdapter == null");
|
if (jsonAdapter == null) throw new IllegalArgumentException("jsonAdapter == null");
|
||||||
|
|
||||||
return add(new JsonAdapter.Factory() {
|
return add(new JsonAdapter.Factory() {
|
||||||
@Override public @Nullable JsonAdapter<?> create(
|
@Override public JsonAdapter<?> create(
|
||||||
Type targetType, Set<? extends Annotation> annotations, Moshi moshi) {
|
Type targetType, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||||
return annotations.isEmpty() && Util.typesMatch(type, targetType) ? jsonAdapter : null;
|
return annotations.isEmpty() && Util.typesMatch(type, targetType) ? jsonAdapter : null;
|
||||||
}
|
}
|
||||||
|
@ -215,7 +150,7 @@ public final class Moshi {
|
||||||
}
|
}
|
||||||
|
|
||||||
return add(new JsonAdapter.Factory() {
|
return add(new JsonAdapter.Factory() {
|
||||||
@Override public @Nullable JsonAdapter<?> create(
|
@Override public JsonAdapter<?> create(
|
||||||
Type targetType, Set<? extends Annotation> annotations, Moshi moshi) {
|
Type targetType, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||||
if (Util.typesMatch(type, targetType)
|
if (Util.typesMatch(type, targetType)
|
||||||
&& annotations.size() == 1
|
&& annotations.size() == 1
|
||||||
|
@ -227,154 +162,49 @@ public final class Moshi {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder add(JsonAdapter.Factory factory) {
|
public Builder add(JsonAdapter.Factory jsonAdapter) {
|
||||||
if (factory == null) throw new IllegalArgumentException("factory == null");
|
factories.add(jsonAdapter);
|
||||||
factories.add(factory);
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder add(Object adapter) {
|
public Builder add(Object adapter) {
|
||||||
if (adapter == null) throw new IllegalArgumentException("adapter == null");
|
|
||||||
return add(AdapterMethodsFactory.get(adapter));
|
return add(AdapterMethodsFactory.get(adapter));
|
||||||
}
|
}
|
||||||
|
|
||||||
Builder addAll(List<JsonAdapter.Factory> factories) {
|
public Moshi build() {
|
||||||
this.factories.addAll(factories);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@CheckReturnValue public Moshi build() {
|
|
||||||
return new Moshi(this);
|
return new Moshi(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A possibly-reentrant chain of lookups for JSON adapters.
|
* 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.
|
||||||
*
|
*
|
||||||
* <p>We keep track of the current stack of lookups: we may start by looking up the JSON adapter
|
* <p>Typically this is necessary in self-referential object models, such as an {@code Employee}
|
||||||
* for Employee, re-enter looking for the JSON adapter of HomeAddress, and re-enter again looking
|
* class that has a {@code List<Employee>} field for an organization's management hierarchy.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
final class LookupChain {
|
private static class DeferredAdapter<T> extends JsonAdapter<T> {
|
||||||
final List<Lookup<?>> callLookups = new ArrayList<>();
|
private Object cacheKey;
|
||||||
final Deque<Lookup<?>> stack = new ArrayDeque<>();
|
private JsonAdapter<T> delegate;
|
||||||
boolean exceptionAnnotated;
|
|
||||||
|
|
||||||
/**
|
public 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;
|
this.cacheKey = cacheKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ready(JsonAdapter<T> delegate) {
|
||||||
|
this.delegate = delegate;
|
||||||
|
this.cacheKey = null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override public T fromJson(JsonReader reader) throws IOException {
|
@Override public T fromJson(JsonReader reader) throws IOException {
|
||||||
if (adapter == null) throw new IllegalStateException("JsonAdapter isn't ready");
|
if (delegate == null) throw new IllegalStateException("Type adapter isn't ready");
|
||||||
return adapter.fromJson(reader);
|
return delegate.fromJson(reader);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public void toJson(JsonWriter writer, T value) throws IOException {
|
@Override public void toJson(JsonWriter writer, T value) throws IOException {
|
||||||
if (adapter == null) throw new IllegalStateException("JsonAdapter isn't ready");
|
if (delegate == null) throw new IllegalStateException("Type adapter isn't ready");
|
||||||
adapter.toJson(writer, value);
|
delegate.toJson(writer, value);
|
||||||
}
|
|
||||||
|
|
||||||
@Override public String toString() {
|
|
||||||
return adapter != null ? adapter.toString() : super.toString();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,23 +15,17 @@
|
||||||
*/
|
*/
|
||||||
package com.squareup.moshi;
|
package com.squareup.moshi;
|
||||||
|
|
||||||
import com.squareup.moshi.internal.Util;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import javax.annotation.Nullable;
|
|
||||||
|
|
||||||
import static com.squareup.moshi.internal.Util.generatedAdapter;
|
|
||||||
|
|
||||||
final class StandardJsonAdapters {
|
final class StandardJsonAdapters {
|
||||||
private StandardJsonAdapters() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
|
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
|
||||||
@Override public JsonAdapter<?> create(
|
@Override public JsonAdapter<?> create(
|
||||||
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||||
|
@ -56,15 +50,9 @@ final class StandardJsonAdapters {
|
||||||
if (type == Object.class) return new ObjectJsonAdapter(moshi).nullSafe();
|
if (type == Object.class) return new ObjectJsonAdapter(moshi).nullSafe();
|
||||||
|
|
||||||
Class<?> rawType = Types.getRawType(type);
|
Class<?> rawType = Types.getRawType(type);
|
||||||
|
|
||||||
@Nullable JsonAdapter<?> generatedAdapter = generatedAdapter(moshi, type, rawType);
|
|
||||||
if (generatedAdapter != null) {
|
|
||||||
return generatedAdapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rawType.isEnum()) {
|
if (rawType.isEnum()) {
|
||||||
//noinspection unchecked
|
//noinspection unchecked
|
||||||
return new EnumJsonAdapter<>((Class<? extends Enum>) rawType).nullSafe();
|
return enumAdapter((Class<? extends Enum>) rawType).nullSafe();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -72,7 +60,7 @@ final class StandardJsonAdapters {
|
||||||
|
|
||||||
private static final String ERROR_FORMAT = "Expected %s but was %s at path %s";
|
private static final String ERROR_FORMAT = "Expected %s but was %s at path %s";
|
||||||
|
|
||||||
static int rangeCheckNextInt(JsonReader reader, String typeMessage, int min, int max)
|
private static int rangeCheckNextInt(JsonReader reader, String typeMessage, int min, int max)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
int value = reader.nextInt();
|
int value = reader.nextInt();
|
||||||
if (value < min || value > max) {
|
if (value < min || value > max) {
|
||||||
|
@ -88,7 +76,7 @@ final class StandardJsonAdapters {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public void toJson(JsonWriter writer, Boolean value) throws IOException {
|
@Override public void toJson(JsonWriter writer, Boolean value) throws IOException {
|
||||||
writer.value(value.booleanValue());
|
writer.value(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public String toString() {
|
@Override public String toString() {
|
||||||
|
@ -224,47 +212,27 @@ final class StandardJsonAdapters {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
static final class EnumJsonAdapter<T extends Enum<T>> extends JsonAdapter<T> {
|
static <T extends Enum<T>> JsonAdapter<T> enumAdapter(final Class<T> enumType) {
|
||||||
private final Class<T> enumType;
|
return new JsonAdapter<T>() {
|
||||||
private final String[] nameStrings;
|
@Override public T fromJson(JsonReader reader) throws IOException {
|
||||||
private final T[] constants;
|
String name = reader.nextString();
|
||||||
private final JsonReader.Options options;
|
try {
|
||||||
|
return Enum.valueOf(enumType, name);
|
||||||
EnumJsonAdapter(Class<T> enumType) {
|
} catch (IllegalArgumentException e) {
|
||||||
this.enumType = enumType;
|
throw new JsonDataException("Expected one of "
|
||||||
try {
|
+ Arrays.toString(enumType.getEnumConstants()) + " but was " + name + " at path "
|
||||||
constants = enumType.getEnumConstants();
|
+ reader.getPath());
|
||||||
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("Missing field in " + enumType.getName(), e);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override public T fromJson(JsonReader reader) throws IOException {
|
@Override public void toJson(JsonWriter writer, T value) throws IOException {
|
||||||
int index = reader.selectString(options);
|
writer.value(value.name());
|
||||||
if (index != -1) return constants[index];
|
}
|
||||||
|
|
||||||
// We can consume the string safely, we are terminating anyway.
|
@Override public String toString() {
|
||||||
String path = reader.getPath();
|
return "JsonAdapter(" + enumType.getName() + ")";
|
||||||
String name = reader.nextString();
|
}
|
||||||
throw new JsonDataException("Expected one of "
|
};
|
||||||
+ Arrays.asList(nameStrings) + " but was " + name + " at path " + path);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void toJson(JsonWriter writer, T value) throws IOException {
|
|
||||||
writer.value(nameStrings[value.ordinal()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public String toString() {
|
|
||||||
return "JsonAdapter(" + enumType.getName() + ")";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -277,44 +245,46 @@ final class StandardJsonAdapters {
|
||||||
*/
|
*/
|
||||||
static final class ObjectJsonAdapter extends JsonAdapter<Object> {
|
static final class ObjectJsonAdapter extends JsonAdapter<Object> {
|
||||||
private final Moshi moshi;
|
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;
|
|
||||||
|
|
||||||
ObjectJsonAdapter(Moshi moshi) {
|
public ObjectJsonAdapter(Moshi moshi) {
|
||||||
this.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 {
|
@Override public Object fromJson(JsonReader reader) throws IOException {
|
||||||
switch (reader.peek()) {
|
switch (reader.peek()) {
|
||||||
case BEGIN_ARRAY:
|
case BEGIN_ARRAY:
|
||||||
return listJsonAdapter.fromJson(reader);
|
List<Object> list = new ArrayList<>();
|
||||||
|
reader.beginArray();
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
list.add(fromJson(reader));
|
||||||
|
}
|
||||||
|
reader.endArray();
|
||||||
|
return list;
|
||||||
|
|
||||||
case BEGIN_OBJECT:
|
case BEGIN_OBJECT:
|
||||||
return mapAdapter.fromJson(reader);
|
Map<String, Object> map = new LinkedHashTreeMap<>();
|
||||||
|
reader.beginObject();
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
map.put(reader.nextName(), fromJson(reader));
|
||||||
|
}
|
||||||
|
reader.endObject();
|
||||||
|
return map;
|
||||||
|
|
||||||
case STRING:
|
case STRING:
|
||||||
return stringAdapter.fromJson(reader);
|
return reader.nextString();
|
||||||
|
|
||||||
case NUMBER:
|
case NUMBER:
|
||||||
return doubleAdapter.fromJson(reader);
|
return reader.nextDouble();
|
||||||
|
|
||||||
case BOOLEAN:
|
case BOOLEAN:
|
||||||
return booleanAdapter.fromJson(reader);
|
return reader.nextBoolean();
|
||||||
|
|
||||||
case NULL:
|
case NULL:
|
||||||
return reader.nextNull();
|
return reader.nextNull();
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException("Expected a value but was " + reader.peek()
|
||||||
"Expected a value but was " + reader.peek() + " at path " + reader.getPath());
|
+ " at path " + reader.getPath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,112 +15,34 @@
|
||||||
*/
|
*/
|
||||||
package com.squareup.moshi;
|
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.Array;
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.lang.reflect.GenericArrayType;
|
import java.lang.reflect.GenericArrayType;
|
||||||
import java.lang.reflect.InvocationHandler;
|
import java.lang.reflect.GenericDeclaration;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Modifier;
|
||||||
import java.lang.reflect.ParameterizedType;
|
import java.lang.reflect.ParameterizedType;
|
||||||
import java.lang.reflect.Proxy;
|
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.lang.reflect.TypeVariable;
|
import java.lang.reflect.TypeVariable;
|
||||||
import java.lang.reflect.WildcardType;
|
import java.lang.reflect.WildcardType;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
import java.util.Properties;
|
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. */
|
/** Factory methods for types. */
|
||||||
@CheckReturnValue
|
|
||||||
public final class Types {
|
public final class Types {
|
||||||
|
static final Type[] EMPTY_TYPE_ARRAY = new Type[] {};
|
||||||
|
|
||||||
private Types() {
|
private Types() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the generated {@link JsonAdapter} fully qualified class name for a given
|
* Returns a new parameterized type, applying {@code typeArguments} to {@code rawType}.
|
||||||
* {@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 @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.");
|
|
||||||
}
|
|
||||||
if (annotations.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (Annotation annotation : annotations) {
|
|
||||||
if (jsonQualifier.equals(annotation.annotationType())) {
|
|
||||||
Set<? extends Annotation> delegateAnnotations = new LinkedHashSet<>(annotations);
|
|
||||||
delegateAnnotations.remove(annotation);
|
|
||||||
return Collections.unmodifiableSet(delegateAnnotations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a new parameterized type, applying {@code typeArguments} to {@code rawType}. Use this
|
|
||||||
* method if {@code rawType} is not enclosed in another type.
|
|
||||||
*/
|
*/
|
||||||
public static ParameterizedType newParameterizedType(Type rawType, Type... typeArguments) {
|
public static ParameterizedType newParameterizedType(Type rawType, Type... typeArguments) {
|
||||||
return new ParameterizedTypeImpl(null, rawType, typeArguments);
|
return new ParameterizedTypeImpl(null, rawType, typeArguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a new parameterized type, applying {@code typeArguments} to {@code rawType}. Use this
|
|
||||||
* method if {@code rawType} is enclosed in {@code ownerType}.
|
|
||||||
*/
|
|
||||||
public static ParameterizedType newParameterizedTypeWithOwner(
|
|
||||||
Type ownerType, Type rawType, Type... typeArguments) {
|
|
||||||
return new ParameterizedTypeImpl(ownerType, rawType, typeArguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns an array type whose elements are all instances of {@code componentType}. */
|
/** Returns an array type whose elements are all instances of {@code componentType}. */
|
||||||
public static GenericArrayType arrayOf(Type componentType) {
|
public static GenericArrayType arrayOf(Type componentType) {
|
||||||
return new GenericArrayTypeImpl(componentType);
|
return new GenericArrayTypeImpl(componentType);
|
||||||
|
@ -144,6 +66,33 @@ public final class Types {
|
||||||
return new WildcardTypeImpl(new Type[] { Object.class }, new Type[] { bound });
|
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) {
|
||||||
|
ParameterizedType p = (ParameterizedType) type;
|
||||||
|
return new ParameterizedTypeImpl(p.getOwnerType(),
|
||||||
|
p.getRawType(), p.getActualTypeArguments());
|
||||||
|
|
||||||
|
} else if (type instanceof GenericArrayType) {
|
||||||
|
GenericArrayType g = (GenericArrayType) type;
|
||||||
|
return new GenericArrayTypeImpl(g.getGenericComponentType());
|
||||||
|
|
||||||
|
} else if (type instanceof WildcardType) {
|
||||||
|
WildcardType w = (WildcardType) type;
|
||||||
|
return new WildcardTypeImpl(w.getUpperBounds(), w.getLowerBounds());
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return type; // This type is unsupported!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static Class<?> getRawType(Type type) {
|
public static Class<?> getRawType(Type type) {
|
||||||
if (type instanceof Class<?>) {
|
if (type instanceof Class<?>) {
|
||||||
// type is a normal class.
|
// type is a normal class.
|
||||||
|
@ -158,7 +107,7 @@ public final class Types {
|
||||||
return (Class<?>) rawType;
|
return (Class<?>) rawType;
|
||||||
|
|
||||||
} else if (type instanceof GenericArrayType) {
|
} else if (type instanceof GenericArrayType) {
|
||||||
Type componentType = ((GenericArrayType) type).getGenericComponentType();
|
Type componentType = ((GenericArrayType)type).getGenericComponentType();
|
||||||
return Array.newInstance(getRawType(componentType), 0).getClass();
|
return Array.newInstance(getRawType(componentType), 0).getClass();
|
||||||
|
|
||||||
} else if (type instanceof TypeVariable) {
|
} else if (type instanceof TypeVariable) {
|
||||||
|
@ -176,32 +125,16 @@ public final class Types {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
static boolean equal(Object a, Object b) {
|
||||||
* Returns the element type of this collection type.
|
return a == b || (a != null && a.equals(b));
|
||||||
* @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. */
|
/** Returns true if {@code a} and {@code b} are equal. */
|
||||||
public static boolean equals(@Nullable Type a, @Nullable Type b) {
|
static boolean equals(Type a, Type b) {
|
||||||
if (a == b) {
|
if (a == b) {
|
||||||
return true; // Also handles (a == null && b == null).
|
return true; // Also handles (a == null && b == null).
|
||||||
|
|
||||||
} else if (a instanceof Class) {
|
} 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().
|
return a.equals(b); // Class already specifies equals().
|
||||||
|
|
||||||
} else if (a instanceof ParameterizedType) {
|
} else if (a instanceof ParameterizedType) {
|
||||||
|
@ -214,15 +147,11 @@ public final class Types {
|
||||||
Type[] bTypeArguments = pb instanceof ParameterizedTypeImpl
|
Type[] bTypeArguments = pb instanceof ParameterizedTypeImpl
|
||||||
? ((ParameterizedTypeImpl) pb).typeArguments
|
? ((ParameterizedTypeImpl) pb).typeArguments
|
||||||
: pb.getActualTypeArguments();
|
: pb.getActualTypeArguments();
|
||||||
return equals(pa.getOwnerType(), pb.getOwnerType())
|
return equal(pa.getOwnerType(), pb.getOwnerType())
|
||||||
&& pa.getRawType().equals(pb.getRawType())
|
&& pa.getRawType().equals(pb.getRawType())
|
||||||
&& Arrays.equals(aTypeArguments, bTypeArguments);
|
&& Arrays.equals(aTypeArguments, bTypeArguments);
|
||||||
|
|
||||||
} else if (a instanceof GenericArrayType) {
|
} else if (a instanceof GenericArrayType) {
|
||||||
if (b instanceof Class) {
|
|
||||||
return equals(((Class) b).getComponentType(),
|
|
||||||
((GenericArrayType) a).getGenericComponentType());
|
|
||||||
}
|
|
||||||
if (!(b instanceof GenericArrayType)) return false;
|
if (!(b instanceof GenericArrayType)) return false;
|
||||||
GenericArrayType ga = (GenericArrayType) a;
|
GenericArrayType ga = (GenericArrayType) a;
|
||||||
GenericArrayType gb = (GenericArrayType) b;
|
GenericArrayType gb = (GenericArrayType) b;
|
||||||
|
@ -243,87 +172,56 @@ public final class Types {
|
||||||
&& va.getName().equals(vb.getName());
|
&& va.getName().equals(vb.getName());
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// This isn't a supported type.
|
// This isn't a supported type. Could be a generic array type, wildcard type, etc.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private 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();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param clazz the target class to read the {@code fieldName} field annotations from.
|
* Returns the generic supertype for {@code supertype}. For example, given a class {@code
|
||||||
* @param fieldName the target field name on {@code clazz}.
|
* IntegerSet}, the result for when supertype is {@code Set.class} is {@code Set<Integer>} and the
|
||||||
* @return a set of {@link JsonQualifier}-annotated {@link Annotation} instances retrieved from
|
* result when the supertype is {@code Collection.class} is {@code Collection<Integer>}.
|
||||||
* the targeted field. Can be empty if none are found.
|
|
||||||
*/
|
*/
|
||||||
public static Set<? extends Annotation> getFieldJsonQualifierAnnotations(Class<?> clazz,
|
static Type getGenericSupertype(Type context, Class<?> rawType, Class<?> toResolve) {
|
||||||
String fieldName) {
|
if (toResolve == rawType) {
|
||||||
try {
|
return context;
|
||||||
Field field = clazz.getDeclaredField(fieldName);
|
}
|
||||||
field.setAccessible(true);
|
|
||||||
Annotation[] fieldAnnotations = field.getDeclaredAnnotations();
|
// we skip searching through interfaces if unknown is an interface
|
||||||
Set<Annotation> annotations = new LinkedHashSet<>(fieldAnnotations.length);
|
if (toResolve.isInterface()) {
|
||||||
for (Annotation annotation : fieldAnnotations) {
|
Class<?>[] interfaces = rawType.getInterfaces();
|
||||||
if (annotation.annotationType().isAnnotationPresent(JsonQualifier.class)) {
|
for (int i = 0, length = interfaces.length; i < length; i++) {
|
||||||
annotations.add(annotation);
|
if (interfaces[i] == toResolve) {
|
||||||
|
return rawType.getGenericInterfaces()[i];
|
||||||
|
} else if (toResolve.isAssignableFrom(interfaces[i])) {
|
||||||
|
return getGenericSupertype(rawType.getGenericInterfaces()[i], interfaces[i], toResolve);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Collections.unmodifiableSet(annotations);
|
|
||||||
} catch (NoSuchFieldException e) {
|
|
||||||
throw new IllegalArgumentException("Could not access field "
|
|
||||||
+ fieldName
|
|
||||||
+ " on class "
|
|
||||||
+ clazz.getCanonicalName(),
|
|
||||||
e);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
// check our supertypes
|
||||||
static <T extends Annotation> T createJsonQualifierImplementation(final Class<T> annotationType) {
|
if (!rawType.isInterface()) {
|
||||||
if (!annotationType.isAnnotation()) {
|
while (rawType != Object.class) {
|
||||||
throw new IllegalArgumentException(annotationType + " must be an annotation.");
|
Class<?> rawSupertype = rawType.getSuperclass();
|
||||||
|
if (rawSupertype == toResolve) {
|
||||||
|
return rawType.getGenericSuperclass();
|
||||||
|
} else if (toResolve.isAssignableFrom(rawSupertype)) {
|
||||||
|
return getGenericSupertype(rawType.getGenericSuperclass(), rawSupertype, toResolve);
|
||||||
|
}
|
||||||
|
rawType = rawSupertype;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!annotationType.isAnnotationPresent(JsonQualifier.class)) {
|
|
||||||
throw new IllegalArgumentException(annotationType + " must have @JsonQualifier.");
|
|
||||||
}
|
|
||||||
if (annotationType.getDeclaredMethods().length != 0) {
|
|
||||||
throw new IllegalArgumentException(annotationType + " must not declare methods.");
|
|
||||||
}
|
|
||||||
return (T) Proxy.newProxyInstance(annotationType.getClassLoader(),
|
|
||||||
new Class<?>[] { annotationType }, new InvocationHandler() {
|
|
||||||
@Override public Object invoke(Object proxy, Method method, Object[] args)
|
|
||||||
throws Throwable {
|
|
||||||
String methodName = method.getName();
|
|
||||||
switch (methodName) {
|
|
||||||
case "annotationType":
|
|
||||||
return annotationType;
|
|
||||||
case "equals":
|
|
||||||
Object o = args[0];
|
|
||||||
return annotationType.isInstance(o);
|
|
||||||
case "hashCode":
|
|
||||||
return 0;
|
|
||||||
case "toString":
|
|
||||||
return "@" + annotationType.getName() + "()";
|
|
||||||
default:
|
|
||||||
return method.invoke(proxy, args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// we can't resolve this further
|
||||||
* Returns a two element array containing this map's key and value types in positions 0 and 1
|
return toResolve;
|
||||||
* 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 };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -357,4 +255,290 @@ public final class Types {
|
||||||
return null;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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;
|
||||||
|
private final Type[] typeArguments;
|
||||||
|
|
||||||
|
public ParameterizedTypeImpl(Type ownerType, Type rawType, Type... typeArguments) {
|
||||||
|
// require an owner type if the raw type needs it
|
||||||
|
if (rawType instanceof Class<?>) {
|
||||||
|
Class<?> rawTypeAsClass = (Class<?>) rawType;
|
||||||
|
boolean isStaticOrTopLevelClass = Modifier.isStatic(rawTypeAsClass.getModifiers())
|
||||||
|
|| rawTypeAsClass.getEnclosingClass() == null;
|
||||||
|
if (ownerType == null && !isStaticOrTopLevelClass) throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type[] getActualTypeArguments() {
|
||||||
|
return typeArguments.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type getRawType() {
|
||||||
|
return rawType;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type[] getUpperBounds() {
|
||||||
|
return new Type[] { upperBound };
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
66
moshi/src/main/java/com/squareup/moshi/Util.java
Normal file
66
moshi/src/main/java/com/squareup/moshi/Util.java
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,55 +0,0 @@
|
||||||
/*
|
|
||||||
* 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()";
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,523 +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.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
/** Moshi is modern JSON library for Android and Java. */
|
|
||||||
@javax.annotation.ParametersAreNonnullByDefault
|
|
||||||
package com.squareup.moshi;
|
|
|
@ -1,49 +0,0 @@
|
||||||
# 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,24 +15,14 @@
|
||||||
*/
|
*/
|
||||||
package com.squareup.moshi;
|
package com.squareup.moshi;
|
||||||
|
|
||||||
import com.squareup.moshi.MoshiTest.Uppercase;
|
|
||||||
import com.squareup.moshi.MoshiTest.UppercaseAdapterFactory;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.reflect.ParameterizedType;
|
|
||||||
import java.lang.reflect.Type;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
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 org.junit.Test;
|
||||||
|
|
||||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
@ -83,91 +73,6 @@ 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 {
|
@Test public void toJsonOnly() throws Exception {
|
||||||
Moshi moshi = new Moshi.Builder()
|
Moshi moshi = new Moshi.Builder()
|
||||||
.add(new PointAsListOfIntegersToAdapter())
|
.add(new PointAsListOfIntegersToAdapter())
|
||||||
|
@ -278,65 +183,6 @@ public final class AdapterMethodsTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test public void emptyAdapters() throws Exception {
|
|
||||||
Moshi.Builder builder = new Moshi.Builder();
|
|
||||||
try {
|
|
||||||
builder.add(new EmptyJsonAdapter()).build();
|
|
||||||
fail();
|
|
||||||
} catch (IllegalArgumentException expected) {
|
|
||||||
assertThat(expected).hasMessage(
|
|
||||||
"Expected at least one @ToJson or @FromJson method on "
|
|
||||||
+ "com.squareup.moshi.AdapterMethodsTest$EmptyJsonAdapter");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class EmptyJsonAdapter {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test public void unexpectedSignatureToAdapters() throws Exception {
|
|
||||||
Moshi.Builder builder = new Moshi.Builder();
|
|
||||||
try {
|
|
||||||
builder.add(new UnexpectedSignatureToJsonAdapter()).build();
|
|
||||||
fail();
|
|
||||||
} catch (IllegalArgumentException expected) {
|
|
||||||
assertThat(expected).hasMessage("Unexpected signature for void "
|
|
||||||
+ "com.squareup.moshi.AdapterMethodsTest$UnexpectedSignatureToJsonAdapter.pointToJson"
|
|
||||||
+ "(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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class UnexpectedSignatureToJsonAdapter {
|
|
||||||
@ToJson void pointToJson(Point point) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test public void unexpectedSignatureFromAdapters() throws Exception {
|
|
||||||
Moshi.Builder builder = new Moshi.Builder();
|
|
||||||
try {
|
|
||||||
builder.add(new UnexpectedSignatureFromJsonAdapter()).build();
|
|
||||||
fail();
|
|
||||||
} catch (IllegalArgumentException expected) {
|
|
||||||
assertThat(expected).hasMessage("Unexpected signature for void "
|
|
||||||
+ "com.squareup.moshi.AdapterMethodsTest$UnexpectedSignatureFromJsonAdapter.pointFromJson"
|
|
||||||
+ "(java.lang.String).\n"
|
|
||||||
+ "@FromJson method signatures may have one of the following structures:\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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class UnexpectedSignatureFromJsonAdapter {
|
|
||||||
@FromJson void pointFromJson(String point) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple adapter methods are not invoked for null values unless they're annotated {@code
|
* Simple adapter methods are not invoked for null values unless they're annotated {@code
|
||||||
* @Nullable}. (The specific annotation class doesn't matter; just its simple name.)
|
* @Nullable}. (The specific annotation class doesn't matter; just its simple name.)
|
||||||
|
@ -387,33 +233,6 @@ public final class AdapterMethodsTest {
|
||||||
@interface Nullable {
|
@interface Nullable {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test public void toAndFromNullJsonWithWriterAndReader() throws Exception {
|
|
||||||
Moshi moshi = new Moshi.Builder()
|
|
||||||
.add(new NullableIntToJsonAdapter())
|
|
||||||
.build();
|
|
||||||
JsonAdapter<Point> pointAdapter = moshi.adapter(Point.class);
|
|
||||||
assertThat(pointAdapter.fromJson("{\"x\":null,\"y\":3}")).isEqualTo(new Point(-1, 3));
|
|
||||||
assertThat(pointAdapter.toJson(new Point(-1, 3))).isEqualTo("{\"y\":3}");
|
|
||||||
}
|
|
||||||
|
|
||||||
static class NullableIntToJsonAdapter {
|
|
||||||
@FromJson int jsonToInt(JsonReader reader) throws IOException {
|
|
||||||
if (reader.peek() == JsonReader.Token.NULL) {
|
|
||||||
reader.nextNull();
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return reader.nextInt();
|
|
||||||
}
|
|
||||||
|
|
||||||
@ToJson void intToJson(JsonWriter writer, int value) throws IOException {
|
|
||||||
if (value == -1) {
|
|
||||||
writer.nullValue();
|
|
||||||
} else {
|
|
||||||
writer.value(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test public void adapterThrows() throws Exception {
|
@Test public void adapterThrows() throws Exception {
|
||||||
Moshi moshi = new Moshi.Builder()
|
Moshi moshi = new Moshi.Builder()
|
||||||
.add(new ExceptionThrowingPointJsonAdapter())
|
.add(new ExceptionThrowingPointJsonAdapter())
|
||||||
|
@ -437,12 +256,10 @@ public final class AdapterMethodsTest {
|
||||||
|
|
||||||
static class ExceptionThrowingPointJsonAdapter {
|
static class ExceptionThrowingPointJsonAdapter {
|
||||||
@ToJson void pointToJson(JsonWriter writer, Point point) throws Exception {
|
@ToJson void pointToJson(JsonWriter writer, Point point) throws Exception {
|
||||||
if (point != null) throw new Exception("pointToJson fail!");
|
throw new Exception("pointToJson fail!");
|
||||||
writer.nullValue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FromJson Point pointFromJson(JsonReader reader) throws Exception {
|
@FromJson Point pointFromJson(JsonReader reader) throws Exception {
|
||||||
if (reader.peek() == JsonReader.Token.NULL) return reader.nextNull();
|
|
||||||
throw new Exception("pointFromJson fail!");
|
throw new Exception("pointFromJson fail!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -462,10 +279,7 @@ public final class AdapterMethodsTest {
|
||||||
fail();
|
fail();
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
assertThat(e).hasMessage("No @FromJson adapter for interface "
|
assertThat(e).hasMessage("No @FromJson adapter for interface "
|
||||||
+ "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)");
|
+ "com.squareup.moshi.AdapterMethodsTest$Shape annotated []");
|
||||||
assertThat(e).hasCauseExactlyInstanceOf(IllegalArgumentException.class);
|
|
||||||
assertThat(e.getCause()).hasMessage("No next JsonAdapter for interface "
|
|
||||||
+ "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -484,284 +298,7 @@ public final class AdapterMethodsTest {
|
||||||
fail();
|
fail();
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
assertThat(e).hasMessage("No @ToJson adapter for interface "
|
assertThat(e).hasMessage("No @ToJson adapter for interface "
|
||||||
+ "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)");
|
+ "com.squareup.moshi.AdapterMethodsTest$Shape annotated []");
|
||||||
assertThat(e).hasCauseExactlyInstanceOf(IllegalArgumentException.class);
|
|
||||||
assertThat(e.getCause()).hasMessage("No next JsonAdapter for interface "
|
|
||||||
+ "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unfortunately in some versions of Android the implementations of {@link ParameterizedType}
|
|
||||||
* doesn't implement equals and hashCode. Confirm that we work around that.
|
|
||||||
*/
|
|
||||||
@Test public void parameterizedTypeEqualsNotUsed() throws Exception {
|
|
||||||
Moshi moshi = new Moshi.Builder()
|
|
||||||
.add(new ListOfStringJsonAdapter())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// This class doesn't implement equals() and hashCode() as it should.
|
|
||||||
ParameterizedType listOfStringType = brokenParameterizedType(0, List.class, String.class);
|
|
||||||
|
|
||||||
JsonAdapter<List<String>> jsonAdapter = moshi.adapter(listOfStringType);
|
|
||||||
assertThat(jsonAdapter.toJson(Arrays.asList("a", "b", "c"))).isEqualTo("\"a|b|c\"");
|
|
||||||
assertThat(jsonAdapter.fromJson("\"a|b|c\"")).isEqualTo(Arrays.asList("a", "b", "c"));
|
|
||||||
}
|
|
||||||
|
|
||||||
static class ListOfStringJsonAdapter {
|
|
||||||
@ToJson String listOfStringToJson(List<String> list) {
|
|
||||||
StringBuilder result = new StringBuilder();
|
|
||||||
for (int i = 0; i < list.size(); i++) {
|
|
||||||
if (i > 0) result.append('|');
|
|
||||||
result.append(list.get(i));
|
|
||||||
}
|
|
||||||
return result.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@FromJson List<String> listOfStringFromJson(String string) {
|
|
||||||
return Arrays.asList(string.split("\\|"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Even when the types we use to look up JSON adapters are not equal, if they're equivalent they
|
|
||||||
* should return the same JsonAdapter instance.
|
|
||||||
*/
|
|
||||||
@Test public void parameterizedTypeCacheKey() throws Exception {
|
|
||||||
Moshi moshi = new Moshi.Builder().build();
|
|
||||||
|
|
||||||
Type a = brokenParameterizedType(0, List.class, String.class);
|
|
||||||
Type b = brokenParameterizedType(1, List.class, String.class);
|
|
||||||
Type c = brokenParameterizedType(2, List.class, String.class);
|
|
||||||
|
|
||||||
assertThat(moshi.adapter(b)).isSameAs(moshi.adapter(a));
|
|
||||||
assertThat(moshi.adapter(c)).isSameAs(moshi.adapter(a));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test public void writerAndReaderTakingJsonAdapterParameter() throws Exception {
|
|
||||||
Moshi moshi = new Moshi.Builder()
|
|
||||||
.add(new PointWriterAndReaderJsonAdapter())
|
|
||||||
.add(new JsonAdapterWithWriterAndReaderTakingJsonAdapterParameter())
|
|
||||||
.build();
|
|
||||||
JsonAdapter<Line> lineAdapter = moshi.adapter(Line.class);
|
|
||||||
Line line = new Line(new Point(5, 8), new Point(3, 2));
|
|
||||||
assertThat(lineAdapter.toJson(line)).isEqualTo("[[5,8],[3,2]]");
|
|
||||||
assertThat(lineAdapter.fromJson("[[5,8],[3,2]]")).isEqualTo(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
static class JsonAdapterWithWriterAndReaderTakingJsonAdapterParameter {
|
|
||||||
@ToJson void lineToJson(
|
|
||||||
JsonWriter writer, Line line, JsonAdapter<Point> pointAdapter) throws IOException {
|
|
||||||
writer.beginArray();
|
|
||||||
pointAdapter.toJson(writer, line.a);
|
|
||||||
pointAdapter.toJson(writer, line.b);
|
|
||||||
writer.endArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
@FromJson Line lineFromJson(
|
|
||||||
JsonReader reader, JsonAdapter<Point> pointAdapter) throws Exception {
|
|
||||||
reader.beginArray();
|
|
||||||
Point a = pointAdapter.fromJson(reader);
|
|
||||||
Point b = pointAdapter.fromJson(reader);
|
|
||||||
reader.endArray();
|
|
||||||
return new Line(a, b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test public void writerAndReaderTakingAnnotatedJsonAdapterParameter() throws Exception {
|
|
||||||
Moshi moshi = new Moshi.Builder()
|
|
||||||
.add(new PointWithParensJsonAdapter())
|
|
||||||
.add(new JsonAdapterWithWriterAndReaderTakingAnnotatedJsonAdapterParameter())
|
|
||||||
.build();
|
|
||||||
JsonAdapter<Line> lineAdapter = moshi.adapter(Line.class);
|
|
||||||
Line line = new Line(new Point(5, 8), new Point(3, 2));
|
|
||||||
assertThat(lineAdapter.toJson(line)).isEqualTo("[\"(5 8)\",\"(3 2)\"]");
|
|
||||||
assertThat(lineAdapter.fromJson("[\"(5 8)\",\"(3 2)\"]")).isEqualTo(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
static class PointWithParensJsonAdapter{
|
|
||||||
@ToJson String pointToJson(@WithParens Point point) throws IOException {
|
|
||||||
return String.format("(%s %s)", point.x, point.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
@FromJson @WithParens Point pointFromJson(String string) throws Exception {
|
|
||||||
Matcher matcher = Pattern.compile("\\((\\d+) (\\d+)\\)").matcher(string);
|
|
||||||
if (!matcher.matches()) throw new JsonDataException();
|
|
||||||
return new Point(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class JsonAdapterWithWriterAndReaderTakingAnnotatedJsonAdapterParameter {
|
|
||||||
@ToJson void lineToJson(JsonWriter writer, Line line,
|
|
||||||
@WithParens JsonAdapter<Point> pointAdapter) throws IOException {
|
|
||||||
writer.beginArray();
|
|
||||||
pointAdapter.toJson(writer, line.a);
|
|
||||||
pointAdapter.toJson(writer, line.b);
|
|
||||||
writer.endArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
@FromJson Line lineFromJson(
|
|
||||||
JsonReader reader, @WithParens JsonAdapter<Point> pointAdapter) throws Exception {
|
|
||||||
reader.beginArray();
|
|
||||||
Point a = pointAdapter.fromJson(reader);
|
|
||||||
Point b = pointAdapter.fromJson(reader);
|
|
||||||
reader.endArray();
|
|
||||||
return new Line(a, b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test public void writerAndReaderTakingMultipleJsonAdapterParameters() throws Exception {
|
|
||||||
Moshi moshi = new Moshi.Builder()
|
|
||||||
.add(new PointWriterAndReaderJsonAdapter())
|
|
||||||
.add(new PointWithParensJsonAdapter())
|
|
||||||
.add(new JsonAdapterWithWriterAndReaderTakingMultipleJsonAdapterParameters())
|
|
||||||
.build();
|
|
||||||
JsonAdapter<Line> lineAdapter = moshi.adapter(Line.class);
|
|
||||||
Line line = new Line(new Point(5, 8), new Point(3, 2));
|
|
||||||
assertThat(lineAdapter.toJson(line)).isEqualTo("[[5,8],\"(3 2)\"]");
|
|
||||||
assertThat(lineAdapter.fromJson("[[5,8],\"(3 2)\"]")).isEqualTo(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
static class JsonAdapterWithWriterAndReaderTakingMultipleJsonAdapterParameters {
|
|
||||||
@ToJson void lineToJson(JsonWriter writer, Line line,
|
|
||||||
JsonAdapter<Point> aAdapter, @WithParens JsonAdapter<Point> bAdapter) throws IOException {
|
|
||||||
writer.beginArray();
|
|
||||||
aAdapter.toJson(writer, line.a);
|
|
||||||
bAdapter.toJson(writer, line.b);
|
|
||||||
writer.endArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
@FromJson Line lineFromJson(JsonReader reader,
|
|
||||||
JsonAdapter<Point> aAdapter, @WithParens JsonAdapter<Point> bAdapter) throws Exception {
|
|
||||||
reader.beginArray();
|
|
||||||
Point a = aAdapter.fromJson(reader);
|
|
||||||
Point b = bAdapter.fromJson(reader);
|
|
||||||
reader.endArray();
|
|
||||||
return new Line(a, b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Retention(RUNTIME)
|
|
||||||
@JsonQualifier
|
|
||||||
public @interface WithParens {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test public void noToJsonAdapterTakingJsonAdapterParameter() throws Exception {
|
|
||||||
try {
|
|
||||||
new Moshi.Builder().add(new ToJsonAdapterTakingJsonAdapterParameter());
|
|
||||||
fail();
|
|
||||||
} catch (IllegalArgumentException expected) {
|
|
||||||
assertThat(expected).hasMessageStartingWith("Unexpected signature");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class ToJsonAdapterTakingJsonAdapterParameter {
|
|
||||||
@ToJson String lineToJson(Line line, JsonAdapter<Point> pointAdapter) throws IOException {
|
|
||||||
throw new AssertionError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test public void noFromJsonAdapterTakingJsonAdapterParameter() throws Exception {
|
|
||||||
try {
|
|
||||||
new Moshi.Builder().add(new FromJsonAdapterTakingJsonAdapterParameter());
|
|
||||||
fail();
|
|
||||||
} catch (IllegalArgumentException expected) {
|
|
||||||
assertThat(expected).hasMessageStartingWith("Unexpected signature");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class FromJsonAdapterTakingJsonAdapterParameter {
|
|
||||||
@FromJson Line lineFromJson(String value, JsonAdapter<Point> pointAdapter) throws Exception {
|
|
||||||
throw new AssertionError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test public void adaptedTypeIsEnclosedParameterizedType() throws Exception {
|
|
||||||
Moshi moshi = new Moshi.Builder()
|
|
||||||
.add(new EnclosedParameterizedTypeJsonAdapter())
|
|
||||||
.build();
|
|
||||||
JsonAdapter<Box<Point>> boxAdapter = moshi.adapter(Types.newParameterizedTypeWithOwner(
|
|
||||||
AdapterMethodsTest.class, Box.class, Point.class));
|
|
||||||
Box<Point> box = new Box<>(new Point(5, 8));
|
|
||||||
String json = "[{\"x\":5,\"y\":8}]";
|
|
||||||
assertThat(boxAdapter.toJson(box)).isEqualTo(json);
|
|
||||||
assertThat(boxAdapter.fromJson(json)).isEqualTo(box);
|
|
||||||
}
|
|
||||||
|
|
||||||
static class EnclosedParameterizedTypeJsonAdapter {
|
|
||||||
@FromJson Box<Point> boxFromJson(List<Point> points) {
|
|
||||||
return new Box<>(points.get(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
@ToJson List<Point> boxToJson(Box<Point> box) throws Exception {
|
|
||||||
return Collections.singletonList(box.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class Box<T> {
|
|
||||||
final T data;
|
|
||||||
|
|
||||||
public Box(T data) {
|
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public boolean equals(Object o) {
|
|
||||||
return o instanceof Box && ((Box) o).data.equals(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public int hashCode() {
|
|
||||||
return data.hashCode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -783,55 +320,7 @@ public final class AdapterMethodsTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static class Line {
|
|
||||||
final Point a;
|
|
||||||
final Point b;
|
|
||||||
|
|
||||||
public Line(Point a, Point b) {
|
|
||||||
this.a = a;
|
|
||||||
this.b = b;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public boolean equals(Object o) {
|
|
||||||
return o instanceof Line && ((Line) o).a.equals(a) && ((Line) o).b.equals(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public int hashCode() {
|
|
||||||
return a.hashCode() * 37 + b.hashCode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Shape {
|
interface Shape {
|
||||||
String draw();
|
String draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a new parameterized type that doesn't implement {@link Object#equals} or {@link
|
|
||||||
* Object#hashCode} by value. These implementation defects are consistent with the parameterized
|
|
||||||
* type that shipped in some older versions of Android.
|
|
||||||
*/
|
|
||||||
ParameterizedType brokenParameterizedType(
|
|
||||||
final int hashCode, final Class<?> rawType, final Type... typeArguments) {
|
|
||||||
return new ParameterizedType() {
|
|
||||||
@Override public Type[] getActualTypeArguments() {
|
|
||||||
return typeArguments;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public Type getRawType() {
|
|
||||||
return rawType;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public Type getOwnerType() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public boolean equals(Object other) {
|
|
||||||
return other == this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public int hashCode() {
|
|
||||||
return hashCode;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
*/
|
*/
|
||||||
package com.squareup.moshi;
|
package com.squareup.moshi;
|
||||||
|
|
||||||
import com.squareup.moshi.internal.Util;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
|
|
|
@ -25,7 +25,7 @@ import okio.Buffer;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import static com.squareup.moshi.TestUtil.newReader;
|
import static com.squareup.moshi.TestUtil.newReader;
|
||||||
import static com.squareup.moshi.internal.Util.NO_ANNOTATIONS;
|
import static com.squareup.moshi.Util.NO_ANNOTATIONS;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
@ -341,23 +341,8 @@ 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 {
|
@Test public void interfaceNotSupported() throws Exception {
|
||||||
assertThat(ClassJsonAdapter.FACTORY.create(Interface.class, NO_ANNOTATIONS, moshi)).isNull();
|
assertThat(ClassJsonAdapter.FACTORY.create(Runnable.class, NO_ANNOTATIONS, moshi)).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
static abstract class Abstract {
|
static abstract class Abstract {
|
||||||
|
@ -444,23 +429,6 @@ public final class ClassJsonAdapterTest {
|
||||||
assertThat(fromJson.zipCode).isEqualTo("94043");
|
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 {
|
private <T> String toJson(Class<T> type, T value) throws IOException {
|
||||||
@SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument.
|
@SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument.
|
||||||
JsonAdapter<T> jsonAdapter = (JsonAdapter<T>) ClassJsonAdapter.FACTORY.create(
|
JsonAdapter<T> jsonAdapter = (JsonAdapter<T>) ClassJsonAdapter.FACTORY.create(
|
||||||
|
|
|
@ -1,120 +0,0 @@
|
||||||
/*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,338 +0,0 @@
|
||||||
/*
|
|
||||||
* 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\"}]");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,288 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 java.io.IOException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
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;
|
|
||||||
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;
|
|
||||||
import static org.junit.Assume.assumeTrue;
|
|
||||||
|
|
||||||
@RunWith(Parameterized.class)
|
|
||||||
public final class JsonAdapterTest {
|
|
||||||
@Parameter public JsonCodecFactory factory;
|
|
||||||
|
|
||||||
@Parameters(name = "{0}")
|
|
||||||
public static List<Object[]> parameters() {
|
|
||||||
return JsonCodecFactory.factories();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test public void lenient() throws Exception {
|
|
||||||
JsonAdapter<Double> lenient = new JsonAdapter<Double>() {
|
|
||||||
@Override public Double fromJson(JsonReader reader) throws IOException {
|
|
||||||
return reader.nextDouble();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void toJson(JsonWriter writer, Double value) throws IOException {
|
|
||||||
writer.value(value);
|
|
||||||
}
|
|
||||||
}.lenient();
|
|
||||||
|
|
||||||
JsonReader reader = factory.newReader("[-Infinity, NaN, Infinity]");
|
|
||||||
reader.beginArray();
|
|
||||||
assertThat(lenient.fromJson(reader)).isEqualTo(Double.NEGATIVE_INFINITY);
|
|
||||||
assertThat(lenient.fromJson(reader)).isNaN();
|
|
||||||
assertThat(lenient.fromJson(reader)).isEqualTo(Double.POSITIVE_INFINITY);
|
|
||||||
reader.endArray();
|
|
||||||
|
|
||||||
JsonWriter writer = factory.newWriter();
|
|
||||||
writer.beginArray();
|
|
||||||
lenient.toJson(writer, Double.NEGATIVE_INFINITY);
|
|
||||||
lenient.toJson(writer, Double.NaN);
|
|
||||||
lenient.toJson(writer, Double.POSITIVE_INFINITY);
|
|
||||||
writer.endArray();
|
|
||||||
assertThat(factory.json()).isEqualTo("[-Infinity,NaN,Infinity]");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test public void nullSafe() 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));
|
|
||||||
}
|
|
||||||
}.nullSafe();
|
|
||||||
|
|
||||||
JsonReader reader = factory.newReader("[\"a\", null, \"c\"]");
|
|
||||||
reader.beginArray();
|
|
||||||
assertThat(toUpperCase.fromJson(reader)).isEqualTo("A");
|
|
||||||
assertThat(toUpperCase.fromJson(reader)).isNull();
|
|
||||||
assertThat(toUpperCase.fromJson(reader)).isEqualTo("C");
|
|
||||||
reader.endArray();
|
|
||||||
|
|
||||||
JsonWriter writer = factory.newWriter();
|
|
||||||
writer.beginArray();
|
|
||||||
toUpperCase.toJson(writer, "a");
|
|
||||||
toUpperCase.toJson(writer, null);
|
|
||||||
toUpperCase.toJson(writer, "c");
|
|
||||||
writer.endArray();
|
|
||||||
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 {
|
|
||||||
reader.skipValue();
|
|
||||||
throw new AssertionError();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void toJson(JsonWriter writer, String value) throws IOException {
|
|
||||||
throw new AssertionError();
|
|
||||||
}
|
|
||||||
}.failOnUnknown();
|
|
||||||
|
|
||||||
JsonReader reader = factory.newReader("[\"a\"]");
|
|
||||||
reader.beginArray();
|
|
||||||
try {
|
|
||||||
alwaysSkip.fromJson(reader);
|
|
||||||
fail();
|
|
||||||
} catch (JsonDataException expected) {
|
|
||||||
assertThat(expected).hasMessage("Cannot skip unexpected STRING at $[0]");
|
|
||||||
}
|
|
||||||
assertThat(reader.nextString()).isEqualTo("a");
|
|
||||||
reader.endArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test public void indent() throws Exception {
|
|
||||||
assumeTrue(factory.encodesToBytes());
|
|
||||||
|
|
||||||
JsonAdapter<List<String>> indent = new JsonAdapter<List<String>>() {
|
|
||||||
@Override public List<String> fromJson(JsonReader reader) throws IOException {
|
|
||||||
throw new AssertionError();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void toJson(JsonWriter writer, List<String> value) throws IOException {
|
|
||||||
writer.beginArray();
|
|
||||||
for (String s : value) {
|
|
||||||
writer.value(s);
|
|
||||||
}
|
|
||||||
writer.endArray();
|
|
||||||
}
|
|
||||||
}.indent("\t\t\t");
|
|
||||||
|
|
||||||
JsonWriter writer = factory.newWriter();
|
|
||||||
indent.toJson(writer, Arrays.asList("a", "b", "c"));
|
|
||||||
assertThat(factory.json()).isEqualTo(""
|
|
||||||
+ "[\n"
|
|
||||||
+ "\t\t\t\"a\",\n"
|
|
||||||
+ "\t\t\t\"b\",\n"
|
|
||||||
+ "\t\t\t\"c\"\n"
|
|
||||||
+ "]");
|
|
||||||
}
|
|
||||||
|
|
||||||
@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 {
|
|
||||||
throw new AssertionError();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public void toJson(JsonWriter writer, Map<String, String> map) throws IOException {
|
|
||||||
writer.beginObject();
|
|
||||||
for (Map.Entry<String, String> entry : map.entrySet()) {
|
|
||||||
writer.name(entry.getKey()).value(entry.getValue());
|
|
||||||
}
|
|
||||||
writer.endObject();
|
|
||||||
}
|
|
||||||
}.serializeNulls();
|
|
||||||
|
|
||||||
JsonWriter writer = factory.newWriter();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,150 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 java.io.IOException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import okio.Buffer;
|
|
||||||
|
|
||||||
abstract class JsonCodecFactory {
|
|
||||||
private static final Moshi MOSHI = new Moshi.Builder().build();
|
|
||||||
private static final JsonAdapter<Object> OBJECT_ADAPTER = MOSHI.adapter(Object.class);
|
|
||||||
|
|
||||||
static List<Object[]> factories() {
|
|
||||||
final JsonCodecFactory utf8 = new JsonCodecFactory() {
|
|
||||||
Buffer buffer;
|
|
||||||
|
|
||||||
@Override public JsonReader newReader(String json) {
|
|
||||||
Buffer buffer = new Buffer().writeUtf8(json);
|
|
||||||
return JsonReader.of(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override JsonWriter newWriter() {
|
|
||||||
buffer = new Buffer();
|
|
||||||
return new JsonUtf8Writer(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override String json() {
|
|
||||||
String result = buffer.readUtf8();
|
|
||||||
buffer = null;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override boolean encodesToBytes() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public String toString() {
|
|
||||||
return "Utf8";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
final JsonCodecFactory value = new JsonCodecFactory() {
|
|
||||||
JsonValueWriter writer;
|
|
||||||
|
|
||||||
@Override public JsonReader newReader(String json) throws IOException {
|
|
||||||
Moshi moshi = new Moshi.Builder().build();
|
|
||||||
Object object = moshi.adapter(Object.class).lenient().fromJson(json);
|
|
||||||
return new JsonValueReader(object);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(jwilson): fix precision checks and delete his method.
|
|
||||||
@Override boolean implementsStrictPrecision() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override JsonWriter newWriter() {
|
|
||||||
writer = new JsonValueWriter();
|
|
||||||
return writer;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override String json() {
|
|
||||||
// This writer writes a DOM. Use other Moshi features to serialize it as a string.
|
|
||||||
try {
|
|
||||||
Buffer buffer = new Buffer();
|
|
||||||
JsonWriter bufferedSinkWriter = JsonWriter.of(buffer);
|
|
||||||
bufferedSinkWriter.setSerializeNulls(true);
|
|
||||||
bufferedSinkWriter.setLenient(true);
|
|
||||||
OBJECT_ADAPTER.toJson(bufferedSinkWriter, writer.root());
|
|
||||||
return buffer.readUtf8();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new AssertionError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(jwilson): support BigDecimal and BigInteger and delete his method.
|
|
||||||
@Override boolean supportsBigNumbers() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override public String toString() {
|
|
||||||
return "Value";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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[] { valuePeek });
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract JsonReader newReader(String json) throws IOException;
|
|
||||||
|
|
||||||
abstract JsonWriter newWriter();
|
|
||||||
|
|
||||||
boolean implementsStrictPrecision() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract String json();
|
|
||||||
|
|
||||||
boolean encodesToBytes() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean supportsBigNumbers() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue