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.
|
||||
#
|
||||
# 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"
|
||||
JDK="openjdk8"
|
||||
JDK="oraclejdk8"
|
||||
BRANCH="master"
|
||||
|
||||
set -e
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -12,7 +12,6 @@ lib
|
|||
target
|
||||
pom.xml.*
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
|
||||
.idea
|
||||
*.iml
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
language: java
|
||||
|
||||
jdk:
|
||||
- openjdk8
|
||||
- oraclejdk7
|
||||
- oraclejdk8
|
||||
|
||||
after_success:
|
||||
- .buildscript/deploy_snapshot.sh
|
||||
|
@ -18,6 +19,8 @@ branches:
|
|||
notifications:
|
||||
email: false
|
||||
|
||||
sudo: false
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.m2
|
||||
|
|
328
CHANGELOG.md
328
CHANGELOG.md
|
@ -1,315 +1,43 @@
|
|||
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
|
||||
|
||||
_2015-09-27_
|
||||
|
||||
* **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
|
||||
call the static factory method instead.
|
||||
* **API Change**: Don’t throw `IOException` on `JsonAdapter.toJson(T)`. Code that calls this
|
||||
method 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.
|
||||
* New: `@Json` annotation can customize a field's name. This is particularly handy for fields
|
||||
whose names are Java keywords, like `default` or `public`.
|
||||
* 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`
|
||||
subproject. You will need to register this adapter if you want this date formatting behavior.
|
||||
See it in action in the [dates example][dates_example].
|
||||
* 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.
|
||||
* New: The `Types` factory class makes it possible to compose types like `List<Card>` or
|
||||
`Map<String, Integer>`. This is useful to look up JSON adapters for parameterized types.
|
||||
* New: `JsonAdapter.failOnUnknown()` returns a new JSON adapter that throws if an unknown value is
|
||||
encountered on the stream. Use this in development and debug builds to detect typos in field
|
||||
names. This feature shouldn’t be used in production because it makes migrations very difficult.
|
||||
* **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
|
||||
call the static factory method instead.
|
||||
* **API Change**: Don’t throw `IOException` on `JsonAdapter.toJson(T)`. Code that calls this method
|
||||
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.
|
||||
* New: `@Json` annotation can customize a field's name. This is particularly handy for fields whose
|
||||
names are Java keywords, like `default` or `public`.
|
||||
* 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` subproject.
|
||||
You will need to register this adapter if you want this date formatting behavior. See it in
|
||||
action in the [dates example][dates_example].
|
||||
* 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.
|
||||
* New: The `Types` factory class makes it possible to compose types like `List<Card>` or
|
||||
`Map<String, Integer>`. This is useful to look up JSON adapters for parameterized types.
|
||||
* New: `JsonAdapter.failOnUnknown()` returns a new JSON adapter that throws if an unknonw value is
|
||||
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.
|
||||
|
||||
## Version 0.9.0
|
||||
|
||||
_2015-06-16_
|
||||
|
||||
* Databinding for primitive types, strings, enums, arrays, collections, and maps.
|
||||
* Databinding for plain old Java objects.
|
||||
* [JSONPath](http://goessner.net/articles/JsonPath/) support for both `JsonReader` and
|
||||
`JsonWriter`.
|
||||
* Throw `JsonDataException` when there’s a data binding problem.
|
||||
* Adapter methods: `@ToJson` and `@FromJson`.
|
||||
* Qualifier annotations: `@JsonQualifier` to permit different type adapters for the same Java
|
||||
type.
|
||||
* Imported code from Gson: `JsonReader`, `JsonWriter`. Also some internal classes:
|
||||
`LinkedHashTreeMap` for hash-collision avoidance and `Types` for typesafe databinding.
|
||||
* Databinding for primitive types, strings, enums, arrays, collections, and maps.
|
||||
* Databinding for plain old Java objects.
|
||||
* [JSONPath](http://goessner.net/articles/JsonPath/) support for both `JsonReader` and
|
||||
`JsonWriter`.
|
||||
* Throw `JsonDataException` when there’s a data binding problem.
|
||||
* Adapter methods: `@ToJson` and `@FromJson`.
|
||||
* Qualifier annotations: `@JsonQualifier` to permit different type adapters for the same Java type.
|
||||
* Imported code from Gson: `JsonReader`, `JsonWriter`. Also some internal classes:
|
||||
`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
|
||||
[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();
|
||||
```
|
||||
|
||||
Voilà:
|
||||
Voila:
|
||||
|
||||
```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
|
||||
|
||||
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
|
||||
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 [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
|
||||
<dependency>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi</artifactId>
|
||||
<version>1.8.0</version>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
```
|
||||
or Gradle:
|
||||
```kotlin
|
||||
implementation("com.squareup.moshi:moshi:1.8.0")
|
||||
```groovy
|
||||
compile 'com.squareup.moshi:moshi:1.0.0'
|
||||
```
|
||||
|
||||
Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap].
|
||||
|
||||
|
||||
R8 / ProGuard
|
||||
--------
|
||||
|
||||
If you are using R8 or ProGuard add the options from [this file](https://github.com/square/moshi/blob/master/moshi/src/main/resources/META-INF/proguard/moshi.pro). If using Android, this requires Android Gradle Plugin 3.2.0+.
|
||||
|
||||
The `moshi-kotlin` artifact additionally requires the options from [this file](https://github.com/square/moshi/blob/master/kotlin/reflect/src/main/resources/META-INF/proguard/moshi-kotlin.pro)
|
||||
|
||||
You might also need rules for Okio which is a dependency of this library.
|
||||
|
||||
License
|
||||
--------
|
||||
|
||||
|
@ -638,10 +216,9 @@ 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/
|
||||
[okio]: https://github.com/square/okio/
|
||||
[okhttp]: https://github.com/square/okhttp/
|
||||
[gson]: https://github.com/google/gson/
|
||||
[javadoc]: https://square.github.io/moshi/1.x/moshi/
|
||||
[kapt]: https://kotlinlang.org/docs/reference/kapt.html
|
||||
[okhttp]: https://github.com/square/okhttp
|
||||
[gson]: https://github.com/google/gson
|
||||
[javadoc]: https://square.github.io/moshi/
|
||||
|
|
|
@ -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>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi-parent</artifactId>
|
||||
<version>1.9.0-SNAPSHOT</version>
|
||||
<version>1.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>moshi-adapters</artifactId>
|
||||
|
@ -17,11 +17,6 @@
|
|||
<artifactId>moshi</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.findbugs</groupId>
|
||||
<artifactId>jsr305</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
|
@ -33,20 +28,4 @@
|
|||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifestEntries>
|
||||
<Automatic-Module-Name>com.squareup.moshi.adapters</Automatic-Module-Name>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
|
|
@ -13,9 +13,8 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* 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.Date;
|
||||
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:
|
||||
* 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
|
||||
* 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.
|
||||
// Other exception types can convert here.
|
||||
} 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;
|
||||
|
||||
/**
|
||||
* @deprecated this class moved to avoid a package name conflict in the Java Platform Module System.
|
||||
* The new class is {@code com.squareup.moshi.adapters.Rfc3339DateJsonAdapter}.
|
||||
* 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}.
|
||||
*/
|
||||
public final class Rfc3339DateJsonAdapter extends JsonAdapter<Date> {
|
||||
com.squareup.moshi.adapters.Rfc3339DateJsonAdapter delegate
|
||||
= new com.squareup.moshi.adapters.Rfc3339DateJsonAdapter();
|
||||
|
||||
@Override public Date fromJson(JsonReader reader) throws IOException {
|
||||
return delegate.fromJson(reader);
|
||||
@Override public synchronized Date fromJson(JsonReader reader) throws IOException {
|
||||
String string = reader.nextString();
|
||||
return Iso8601Utils.parse(string);
|
||||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, Date value) throws IOException {
|
||||
delegate.toJson(writer, value);
|
||||
@Override public synchronized void toJson(JsonWriter writer, Date value) throws IOException {
|
||||
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
|
||||
* 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.Date;
|
||||
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"?>
|
||||
<!DOCTYPE module PUBLIC
|
||||
"-//Puppy Crawl//DTD Check Configuration 1.3//EN"
|
||||
"http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
|
||||
"-//Puppy Crawl//DTD Check Configuration 1.2//EN"
|
||||
"http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
|
||||
|
||||
<module name="Checker">
|
||||
<module name="SuppressWarningsFilter"/>
|
||||
<module name="NewlineAtEndOfFile"/>
|
||||
<module name="FileLength"/>
|
||||
<module name="FileTabCharacter"/>
|
||||
|
@ -45,7 +44,7 @@
|
|||
<module name="LocalVariableName"/>
|
||||
<module name="MemberName"/>
|
||||
<module name="MethodName"/>
|
||||
<!--<module name="PackageName"/>-->
|
||||
<module name="PackageName"/>
|
||||
<module name="ParameterName"/>
|
||||
<module name="StaticVariableName"/>
|
||||
<module name="TypeName"/>
|
||||
|
@ -57,9 +56,7 @@
|
|||
<module name="IllegalImport"/>
|
||||
<!-- defaults to sun.* packages -->
|
||||
<module name="RedundantImport"/>
|
||||
<module name="UnusedImports">
|
||||
<property name="processJavadoc" value="true"/>
|
||||
</module>
|
||||
<module name="UnusedImports"/>
|
||||
|
||||
|
||||
<!-- Checks for Size Violations. -->
|
||||
|
@ -67,9 +64,7 @@
|
|||
<module name="LineLength">
|
||||
<property name="max" value="100"/>
|
||||
</module>
|
||||
<module name="MethodLength">
|
||||
<property name="max" value="200"/>
|
||||
</module>
|
||||
<module name="MethodLength"/>
|
||||
|
||||
|
||||
<!-- Checks for whitespace -->
|
||||
|
@ -83,15 +78,7 @@
|
|||
<module name="ParenPad"/>
|
||||
<module name="TypecastParenPad"/>
|
||||
<module name="WhitespaceAfter"/>
|
||||
<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>
|
||||
<module name="WhitespaceAround"/>
|
||||
|
||||
|
||||
<!-- Modifier Checks -->
|
||||
|
@ -121,7 +108,7 @@
|
|||
<!--module name="InnerAssignment"/-->
|
||||
<!--module name="MagicNumber"/-->
|
||||
<!--module name="MissingSwitchDefault"/-->
|
||||
<!--<module name="RedundantThrows"/>-->
|
||||
<module name="RedundantThrows"/>
|
||||
<module name="SimplifyBooleanExpression"/>
|
||||
<module name="SimplifyBooleanReturn"/>
|
||||
|
||||
|
@ -140,8 +127,5 @@
|
|||
<!--module name="FinalParameters"/-->
|
||||
<!--module name="TodoComment"/-->
|
||||
<module name="UpperEll"/>
|
||||
|
||||
<!-- Make the @SuppressWarnings annotations available to Checkstyle -->
|
||||
<module name="SuppressWarningsHolder"/>
|
||||
</module>
|
||||
</module>
|
||||
|
|
|
@ -6,17 +6,12 @@
|
|||
<parent>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi-parent</artifactId>
|
||||
<version>1.9.0-SNAPSHOT</version>
|
||||
<version>1.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>moshi-examples</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.google.code.findbugs</groupId>
|
||||
<artifactId>jsr305</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi</artifactId>
|
||||
|
|
|
@ -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.Moshi;
|
||||
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter;
|
||||
import com.squareup.moshi.Rfc3339DateJsonAdapter;
|
||||
import com.squareup.moshi.recipes.models.Tournament;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
|
|
|
@ -21,7 +21,7 @@ import com.squareup.moshi.recipes.models.BlackjackHand;
|
|||
|
||||
public final class ReadJson {
|
||||
public void run() throws Exception {
|
||||
String json = ""
|
||||
String json = ""
|
||||
+ "{\n"
|
||||
+ " \"hidden_card\": {\n"
|
||||
+ " \"rank\": \"6\",\n"
|
||||
|
|
|
@ -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;
|
||||
|
||||
@SuppressWarnings("checkstyle:membername")
|
||||
public final class BlackjackHand {
|
||||
public final Card hidden_card;
|
||||
public final List<Card> visible_cards;
|
||||
|
||||
public BlackjackHand(Card hiddenCard, List<Card> visibleCards) {
|
||||
this.hidden_card = hiddenCard;
|
||||
this.visible_cards = visibleCards;
|
||||
public BlackjackHand(Card hidden_card, List<Card> visible_cards) {
|
||||
this.hidden_card = hidden_card;
|
||||
this.visible_cards = visible_cards;
|
||||
}
|
||||
|
||||
@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>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi-parent</artifactId>
|
||||
<version>1.9.0-SNAPSHOT</version>
|
||||
<version>1.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>moshi</artifactId>
|
||||
|
@ -17,11 +17,6 @@
|
|||
<groupId>com.squareup.okio</groupId>
|
||||
<artifactId>okio</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.findbugs</groupId>
|
||||
<artifactId>jsr305</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
|
@ -33,31 +28,4 @@
|
|||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifestEntries>
|
||||
<Automatic-Module-Name>com.squareup.moshi</Automatic-Module-Name>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>2.10.4</version>
|
||||
<configuration>
|
||||
<excludePackageNames>com.squareup.moshi.internal:com.squareup.moshi.internal.*</excludePackageNames>
|
||||
<links>
|
||||
<link>https://square.github.io/okio/</link>
|
||||
</links>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
|
|
@ -15,32 +15,25 @@
|
|||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import com.squareup.moshi.internal.Util;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static com.squareup.moshi.internal.Util.canonicalize;
|
||||
import static com.squareup.moshi.internal.Util.jsonAnnotations;
|
||||
import static com.squareup.moshi.internal.Util.typeAnnotatedWithAnnotations;
|
||||
|
||||
final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
||||
private final List<AdapterMethod> toAdapters;
|
||||
private final List<AdapterMethod> fromAdapters;
|
||||
|
||||
AdapterMethodsFactory(List<AdapterMethod> toAdapters, List<AdapterMethod> fromAdapters) {
|
||||
public AdapterMethodsFactory(List<AdapterMethod> toAdapters, List<AdapterMethod> fromAdapters) {
|
||||
this.toAdapters = toAdapters;
|
||||
this.fromAdapters = fromAdapters;
|
||||
}
|
||||
|
||||
@Override public @Nullable JsonAdapter<?> create(
|
||||
@Override public JsonAdapter<?> create(
|
||||
final Type type, final Set<? extends Annotation> annotations, final Moshi moshi) {
|
||||
final AdapterMethod toAdapter = get(toAdapters, type, annotations);
|
||||
final AdapterMethod fromAdapter = get(fromAdapters, type, annotations);
|
||||
|
@ -53,17 +46,14 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
|||
} catch (IllegalArgumentException e) {
|
||||
String missingAnnotation = toAdapter == null ? "@ToJson" : "@FromJson";
|
||||
throw new IllegalArgumentException("No " + missingAnnotation + " adapter for "
|
||||
+ typeAnnotatedWithAnnotations(type, annotations), e);
|
||||
+ type + " annotated " + annotations);
|
||||
}
|
||||
} else {
|
||||
delegate = null;
|
||||
}
|
||||
|
||||
if (toAdapter != null) toAdapter.bind(moshi, this);
|
||||
if (fromAdapter != null) fromAdapter.bind(moshi, this);
|
||||
|
||||
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) {
|
||||
delegate.toJson(writer, value);
|
||||
} else if (!toAdapter.nullable && value == null) {
|
||||
|
@ -71,15 +61,16 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
|||
} else {
|
||||
try {
|
||||
toAdapter.toJson(moshi, writer, value);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new AssertionError();
|
||||
} catch (InvocationTargetException e) {
|
||||
Throwable cause = e.getCause();
|
||||
if (cause instanceof IOException) throw (IOException) cause;
|
||||
throw new JsonDataException(cause + " at " + writer.getPath(), cause);
|
||||
if (e.getCause() instanceof IOException) throw (IOException) e.getCause();
|
||||
throw new JsonDataException(e.getCause() + " at " + writer.getPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override public @Nullable Object fromJson(JsonReader reader) throws IOException {
|
||||
@Override public Object fromJson(JsonReader reader) throws IOException {
|
||||
if (fromAdapter == null) {
|
||||
return delegate.fromJson(reader);
|
||||
} else if (!fromAdapter.nullable && reader.peek() == JsonReader.Token.NULL) {
|
||||
|
@ -88,10 +79,11 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
|||
} else {
|
||||
try {
|
||||
return fromAdapter.fromJson(moshi, reader);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new AssertionError();
|
||||
} catch (InvocationTargetException e) {
|
||||
Throwable cause = e.getCause();
|
||||
if (cause instanceof IOException) throw (IOException) cause;
|
||||
throw new JsonDataException(cause + " at " + reader.getPath(), cause);
|
||||
if (e.getCause() instanceof IOException) throw (IOException) e.getCause();
|
||||
throw new JsonDataException(e.getCause() + " at " + reader.getPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -146,46 +138,34 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
|||
*/
|
||||
static AdapterMethod toAdapter(Object adapter, Method method) {
|
||||
method.setAccessible(true);
|
||||
Type[] parameterTypes = method.getGenericParameterTypes();
|
||||
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
|
||||
&& returnType == void.class
|
||||
&& parametersAreJsonAdapters(2, parameterTypes)) {
|
||||
// void pointToJson(JsonWriter jsonWriter, Point point) {
|
||||
// void pointToJson(JsonWriter jsonWriter, Point point, JsonAdapter<?> adapter, ...) {
|
||||
Set<? extends Annotation> qualifierAnnotations = jsonAnnotations(parameterAnnotations[1]);
|
||||
return new AdapterMethod(parameterTypes[1], qualifierAnnotations, adapter, method,
|
||||
parameterTypes.length, 2, true) {
|
||||
@Override public void toJson(Moshi moshi, JsonWriter writer, @Nullable Object value)
|
||||
throws IOException, InvocationTargetException {
|
||||
invoke(writer, value);
|
||||
&& returnType == void.class) {
|
||||
// public void pointToJson(JsonWriter jsonWriter, Point point) throws Exception {
|
||||
Set<? extends Annotation> parameterAnnotations
|
||||
= Util.jsonAnnotations(method.getParameterAnnotations()[1]);
|
||||
return new AdapterMethod(parameterTypes[1], parameterAnnotations, adapter, method, false) {
|
||||
@Override public void toJson(Moshi moshi, JsonWriter writer, Object value)
|
||||
throws IOException, InvocationTargetException, IllegalAccessException {
|
||||
method.invoke(adapter, writer, value);
|
||||
}
|
||||
};
|
||||
|
||||
} else if (parameterTypes.length == 1 && returnType != void.class) {
|
||||
// List<Integer> pointToJson(Point point) {
|
||||
final Set<? extends Annotation> returnTypeAnnotations = jsonAnnotations(method);
|
||||
final Set<? extends Annotation> qualifierAnnotations =
|
||||
jsonAnnotations(parameterAnnotations[0]);
|
||||
// public List<Integer> pointToJson(Point point) throws Exception {
|
||||
final Set<? extends Annotation> returnTypeAnnotations = Util.jsonAnnotations(method);
|
||||
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
|
||||
Set<? extends Annotation> qualifierAnnotations =
|
||||
Util.jsonAnnotations(parameterAnnotations[0]);
|
||||
boolean nullable = Util.hasNullable(parameterAnnotations[0]);
|
||||
return new AdapterMethod(parameterTypes[0], qualifierAnnotations, adapter, method,
|
||||
parameterTypes.length, 1, nullable) {
|
||||
private JsonAdapter<Object> delegate;
|
||||
|
||||
@Override public void bind(Moshi moshi, JsonAdapter.Factory factory) {
|
||||
super.bind(moshi, factory);
|
||||
delegate = Types.equals(parameterTypes[0], returnType)
|
||||
&& qualifierAnnotations.equals(returnTypeAnnotations)
|
||||
? moshi.nextAdapter(factory, returnType, returnTypeAnnotations)
|
||||
: moshi.adapter(returnType, returnTypeAnnotations);
|
||||
}
|
||||
|
||||
@Override public void toJson(Moshi moshi, JsonWriter writer, @Nullable Object value)
|
||||
throws IOException, InvocationTargetException {
|
||||
Object intermediate = invoke(value);
|
||||
return new AdapterMethod(parameterTypes[0], qualifierAnnotations, adapter, method, nullable) {
|
||||
@Override public void toJson(Moshi moshi, JsonWriter writer, Object value)
|
||||
throws IOException, InvocationTargetException, IllegalAccessException {
|
||||
JsonAdapter<Object> delegate = moshi.adapter(returnType, returnTypeAnnotations);
|
||||
Object intermediate = method.invoke(adapter, value);
|
||||
delegate.toJson(writer, intermediate);
|
||||
}
|
||||
};
|
||||
|
@ -194,163 +174,91 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
|||
throw new IllegalArgumentException("Unexpected signature for " + method + ".\n"
|
||||
+ "@ToJson method signatures may have one of the following structures:\n"
|
||||
+ " <any access modifier> void toJson(JsonWriter writer, T value) throws <any>;\n"
|
||||
+ " <any access modifier> void toJson(JsonWriter writer, T value,"
|
||||
+ " JsonAdapter<any> delegate, <any more delegates>) throws <any>;\n"
|
||||
+ " <any access modifier> R toJson(T value) throws <any>;\n");
|
||||
}
|
||||
}
|
||||
|
||||
/** 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
|
||||
* converting an object from JSON.
|
||||
*/
|
||||
static AdapterMethod fromAdapter(Object adapter, Method method) {
|
||||
method.setAccessible(true);
|
||||
final Type returnType = method.getGenericReturnType();
|
||||
final Set<? extends Annotation> returnTypeAnnotations = jsonAnnotations(method);
|
||||
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
|
||||
&& returnType != void.class
|
||||
&& parametersAreJsonAdapters(1, parameterTypes)) {
|
||||
// Point pointFromJson(JsonReader jsonReader) {
|
||||
// Point pointFromJson(JsonReader jsonReader, JsonAdapter<?> adapter, ...) {
|
||||
return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method,
|
||||
parameterTypes.length, 1, true) {
|
||||
&& returnType != void.class) {
|
||||
// public Point pointFromJson(JsonReader jsonReader) throws Exception {
|
||||
Set<? extends Annotation> returnTypeAnnotations = Util.jsonAnnotations(method);
|
||||
return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method, false) {
|
||||
@Override public Object fromJson(Moshi moshi, JsonReader reader)
|
||||
throws IOException, InvocationTargetException {
|
||||
return invoke(reader);
|
||||
throws IOException, IllegalAccessException, InvocationTargetException {
|
||||
return method.invoke(adapter, reader);
|
||||
}
|
||||
};
|
||||
|
||||
} 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
|
||||
= jsonAnnotations(parameterAnnotations[0]);
|
||||
= Util.jsonAnnotations(parameterAnnotations[0]);
|
||||
boolean nullable = Util.hasNullable(parameterAnnotations[0]);
|
||||
return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method,
|
||||
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);
|
||||
}
|
||||
|
||||
return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method, nullable) {
|
||||
@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);
|
||||
return invoke(intermediate);
|
||||
return method.invoke(adapter, intermediate);
|
||||
}
|
||||
};
|
||||
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unexpected signature for " + method + ".\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");
|
||||
+ "@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> R toJson(T value) throws <any>;\n");
|
||||
}
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
for (int i = 0, size = adapterMethods.size(); i < size; 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 null;
|
||||
}
|
||||
|
||||
abstract static class AdapterMethod {
|
||||
static abstract class AdapterMethod {
|
||||
final Type type;
|
||||
final Set<? extends Annotation> annotations;
|
||||
final Object adapter;
|
||||
final Method method;
|
||||
final int adaptersOffset;
|
||||
final JsonAdapter<?>[] jsonAdapters;
|
||||
final boolean nullable;
|
||||
|
||||
AdapterMethod(Type type, Set<? extends Annotation> annotations, Object adapter,
|
||||
Method method, int parameterCount, int adaptersOffset, boolean nullable) {
|
||||
this.type = canonicalize(type);
|
||||
public AdapterMethod(Type type,
|
||||
Set<? extends Annotation> annotations, Object adapter, Method method, boolean nullable) {
|
||||
this.type = type;
|
||||
this.annotations = annotations;
|
||||
this.adapter = adapter;
|
||||
this.method = method;
|
||||
this.adaptersOffset = adaptersOffset;
|
||||
this.jsonAdapters = new JsonAdapter[parameterCount - adaptersOffset];
|
||||
this.nullable = nullable;
|
||||
}
|
||||
|
||||
public void bind(Moshi moshi, JsonAdapter.Factory factory) {
|
||||
if (jsonAdapters.length > 0) {
|
||||
Type[] parameterTypes = method.getGenericParameterTypes();
|
||||
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
|
||||
for (int i = adaptersOffset, size = parameterTypes.length; i < size; i++) {
|
||||
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 {
|
||||
public void toJson(Moshi moshi, JsonWriter writer, Object value)
|
||||
throws IOException, IllegalAccessException, InvocationTargetException {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
public @Nullable Object fromJson(Moshi moshi, JsonReader reader)
|
||||
throws IOException, InvocationTargetException {
|
||||
public Object fromJson(Moshi moshi, JsonReader reader)
|
||||
throws IOException, IllegalAccessException, InvocationTargetException {
|
||||
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.List;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Converts arrays to JSON arrays containing their converted contents. This
|
||||
|
@ -30,7 +29,7 @@ import javax.annotation.Nullable;
|
|||
*/
|
||||
final class ArrayJsonAdapter extends JsonAdapter<Object> {
|
||||
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 elementType = Types.arrayComponentType(type);
|
||||
if (elementType == null) return null;
|
||||
|
@ -70,8 +69,4 @@ final class ArrayJsonAdapter extends JsonAdapter<Object> {
|
|||
}
|
||||
writer.endArray();
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
return elementAdapter + ".array()";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,8 +15,6 @@
|
|||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import com.squareup.moshi.internal.Util;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectStreamClass;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
|
@ -79,7 +77,7 @@ abstract class ClassFactory<T> {
|
|||
// 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 {
|
||||
// private static native int getConstructorId(Class<?> c);
|
||||
// private static native Object newInstance(Class<?> instantiationClass, int methodId);
|
||||
|
@ -104,32 +102,11 @@ abstract class ClassFactory<T> {
|
|||
} catch (IllegalAccessException e) {
|
||||
throw new AssertionError();
|
||||
} catch (InvocationTargetException e) {
|
||||
throw Util.rethrowCause(e);
|
||||
throw new RuntimeException(e);
|
||||
} catch (NoSuchMethodException ignored) {
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,77 +15,48 @@
|
|||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import com.squareup.moshi.internal.Util;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static com.squareup.moshi.internal.Util.resolve;
|
||||
|
||||
/**
|
||||
* Emits a regular class as a JSON object by mapping Java fields to JSON object properties.
|
||||
*
|
||||
* <h3>Platform Types</h3>
|
||||
* 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>
|
||||
* 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
|
||||
* serialization and deserialization unless they are either public or protected.
|
||||
*/
|
||||
final class ClassJsonAdapter<T> extends JsonAdapter<T> {
|
||||
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) {
|
||||
if (!(type instanceof Class) && !(type instanceof ParameterizedType)) {
|
||||
return null;
|
||||
}
|
||||
Class<?> rawType = Types.getRawType(type);
|
||||
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 (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())) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot serialize non-static nested class " + rawType.getName());
|
||||
if (rawType.getSimpleName().isEmpty()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot serialize anonymous class " + rawType.getName());
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot serialize non-static nested class " + rawType.getName());
|
||||
}
|
||||
}
|
||||
if (Modifier.isAbstract(rawType.getModifiers())) {
|
||||
throw new IllegalArgumentException("Cannot serialize abstract class " + rawType.getName());
|
||||
}
|
||||
try {
|
||||
//noinspection unchecked if the Class.forName works, the cast will work.
|
||||
Class<? extends Annotation> metadataClass =
|
||||
(Class<? extends Annotation>) Class.forName("kotlin.Metadata");
|
||||
if (rawType.isAnnotationPresent(metadataClass)) {
|
||||
throw new IllegalArgumentException("Cannot serialize Kotlin type " + rawType.getName()
|
||||
+ ". Reflective serialization of Kotlin classes without using kotlin-reflect has "
|
||||
+ "undefined and unexpected behavior. Please use KotlinJsonAdapter from the "
|
||||
+ "moshi-kotlin artifact or use code gen from the moshi-kotlin-codegen artifact.");
|
||||
}
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
|
||||
}
|
||||
|
||||
ClassFactory<Object> classFactory = ClassFactory.get(rawType);
|
||||
Map<String, FieldBinding<?>> fields = new TreeMap<>();
|
||||
|
@ -99,23 +70,22 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
|
|||
private void createFieldBindings(
|
||||
Moshi moshi, Type type, Map<String, FieldBinding<?>> fieldBindings) {
|
||||
Class<?> rawType = Types.getRawType(type);
|
||||
boolean platformType = Util.isPlatformType(rawType);
|
||||
boolean platformType = isPlatformType(rawType);
|
||||
for (Field field : rawType.getDeclaredFields()) {
|
||||
if (!includeField(platformType, field.getModifiers())) continue;
|
||||
|
||||
// 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);
|
||||
String fieldName = field.getName();
|
||||
JsonAdapter<Object> adapter = moshi.adapter(fieldType, annotations, fieldName);
|
||||
JsonAdapter<Object> adapter = moshi.adapter(fieldType, annotations);
|
||||
|
||||
// Create the binding between field and JSON.
|
||||
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!
|
||||
Json jsonAnnotation = field.getAnnotation(Json.class);
|
||||
String name = jsonAnnotation != null ? jsonAnnotation.name() : fieldName;
|
||||
FieldBinding<Object> fieldBinding = new FieldBinding<>(name, field, adapter);
|
||||
String name = jsonAnnotation != null ? jsonAnnotation.name() : field.getName();
|
||||
FieldBinding<?> replaced = fieldBindings.put(name, fieldBinding);
|
||||
if (replaced != null) {
|
||||
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. */
|
||||
private boolean includeField(boolean platformType, int modifiers) {
|
||||
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 FieldBinding<?>[] fieldsArray;
|
||||
private final JsonReader.Options options;
|
||||
private final Map<String, FieldBinding<?>> jsonFields;
|
||||
|
||||
ClassJsonAdapter(ClassFactory<T> classFactory, Map<String, FieldBinding<?>> fieldsMap) {
|
||||
private ClassJsonAdapter(ClassFactory<T> classFactory, Map<String, FieldBinding<?>> jsonFields) {
|
||||
this.classFactory = classFactory;
|
||||
this.fieldsArray = fieldsMap.values().toArray(new FieldBinding[fieldsMap.size()]);
|
||||
this.options = JsonReader.Options.of(
|
||||
fieldsMap.keySet().toArray(new String[fieldsMap.size()]));
|
||||
this.jsonFields = jsonFields;
|
||||
}
|
||||
|
||||
@Override public T fromJson(JsonReader reader) throws IOException {
|
||||
|
@ -150,7 +127,10 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
|
|||
} catch (InstantiationException e) {
|
||||
throw new RuntimeException(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) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
@ -158,13 +138,13 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
|
|||
try {
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
int index = reader.selectName(options);
|
||||
if (index == -1) {
|
||||
reader.skipName();
|
||||
String name = reader.nextName();
|
||||
FieldBinding<?> fieldBinding = jsonFields.get(name);
|
||||
if (fieldBinding != null) {
|
||||
fieldBinding.read(reader, result);
|
||||
} else {
|
||||
reader.skipValue();
|
||||
continue;
|
||||
}
|
||||
fieldsArray[index].read(reader, result);
|
||||
}
|
||||
reader.endObject();
|
||||
return result;
|
||||
|
@ -176,9 +156,9 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
|
|||
@Override public void toJson(JsonWriter writer, T value) throws IOException {
|
||||
try {
|
||||
writer.beginObject();
|
||||
for (FieldBinding<?> fieldBinding : fieldsArray) {
|
||||
writer.name(fieldBinding.name);
|
||||
fieldBinding.write(writer, value);
|
||||
for (Map.Entry<String, FieldBinding<?>> entry : jsonFields.entrySet()) {
|
||||
writer.name(entry.getKey());
|
||||
entry.getValue().write(writer, value);
|
||||
}
|
||||
writer.endObject();
|
||||
} catch (IllegalAccessException e) {
|
||||
|
@ -191,23 +171,21 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
|
|||
}
|
||||
|
||||
static class FieldBinding<T> {
|
||||
final String name;
|
||||
final Field field;
|
||||
final JsonAdapter<T> adapter;
|
||||
private final Field field;
|
||||
private final JsonAdapter<T> adapter;
|
||||
|
||||
FieldBinding(String name, Field field, JsonAdapter<T> adapter) {
|
||||
this.name = name;
|
||||
public FieldBinding(Field field, JsonAdapter<T> adapter) {
|
||||
this.field = field;
|
||||
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);
|
||||
field.set(value, fieldValue);
|
||||
}
|
||||
|
||||
@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);
|
||||
adapter.toJson(writer, fieldValue);
|
||||
}
|
||||
|
|
|
@ -23,12 +23,11 @@ import java.util.Collection;
|
|||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** Converts collection types to JSON arrays containing their converted contents. */
|
||||
abstract class CollectionJsonAdapter<C extends Collection<T>, T> extends JsonAdapter<C> {
|
||||
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
|
||||
@Override public @Nullable JsonAdapter<?> create(
|
||||
@Override public JsonAdapter<?> create(
|
||||
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
Class<?> rawType = Types.getRawType(type);
|
||||
if (!annotations.isEmpty()) return null;
|
||||
|
@ -47,7 +46,7 @@ abstract class CollectionJsonAdapter<C extends Collection<T>, T> extends JsonAda
|
|||
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);
|
||||
JsonAdapter<T> elementAdapter = moshi.adapter(elementType);
|
||||
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);
|
||||
JsonAdapter<T> elementAdapter = moshi.adapter(elementType);
|
||||
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.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
import static java.lang.annotation.ElementType.METHOD;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
/**
|
||||
* Customizes how a field is encoded as JSON.
|
||||
*
|
||||
* <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.
|
||||
*/
|
||||
/** Customizes how a field is encoded as JSON. */
|
||||
@Target({FIELD, METHOD})
|
||||
@Retention(RUNTIME)
|
||||
@Documented
|
||||
public @interface Json {
|
||||
|
|
|
@ -15,14 +15,10 @@
|
|||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import com.squareup.moshi.internal.NullSafeJsonAdapter;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Type;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Set;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
import javax.annotation.Nullable;
|
||||
import okio.Buffer;
|
||||
import okio.BufferedSink;
|
||||
import okio.BufferedSource;
|
||||
|
@ -31,29 +27,24 @@ import okio.BufferedSource;
|
|||
* Converts Java values to JSON, and JSON values to Java.
|
||||
*/
|
||||
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));
|
||||
}
|
||||
|
||||
@CheckReturnValue public final @Nullable T fromJson(String string) throws IOException {
|
||||
JsonReader reader = JsonReader.of(new Buffer().writeUtf8(string));
|
||||
T result = fromJson(reader);
|
||||
if (!isLenient() && reader.peek() != JsonReader.Token.END_DOCUMENT) {
|
||||
throw new JsonDataException("JSON document was not fully consumed.");
|
||||
}
|
||||
return result;
|
||||
public final T fromJson(String string) throws IOException {
|
||||
return fromJson(new Buffer().writeUtf8(string));
|
||||
}
|
||||
|
||||
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);
|
||||
toJson(writer, value);
|
||||
}
|
||||
|
||||
@CheckReturnValue public final String toJson(@Nullable T value) {
|
||||
public final String toJson(T value) {
|
||||
Buffer buffer = new Buffer();
|
||||
try {
|
||||
toJson(buffer, value);
|
||||
|
@ -63,113 +54,38 @@ public abstract class JsonAdapter<T> {
|
|||
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
|
||||
* nulls.
|
||||
*/
|
||||
@CheckReturnValue public final JsonAdapter<T> nullSafe() {
|
||||
return new NullSafeJsonAdapter<>(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a JSON adapter equal to this JSON adapter, but that refuses null values. If null is
|
||||
* read or written this will throw a {@link JsonDataException}.
|
||||
*
|
||||
* <p>Note that this adapter will not usually be invoked for absent values and so those must be
|
||||
* handled elsewhere. This should only be used to fail on explicit nulls.
|
||||
*/
|
||||
@CheckReturnValue public final JsonAdapter<T> nonNull() {
|
||||
public final JsonAdapter<T> nullSafe() {
|
||||
final JsonAdapter<T> delegate = this;
|
||||
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) {
|
||||
throw new JsonDataException("Unexpected null at " + reader.getPath());
|
||||
return reader.nextNull();
|
||||
} else {
|
||||
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) {
|
||||
throw new JsonDataException("Unexpected null at " + writer.getPath());
|
||||
writer.nullValue();
|
||||
} else {
|
||||
delegate.toJson(writer, value);
|
||||
}
|
||||
}
|
||||
@Override boolean isLenient() {
|
||||
return delegate.isLenient();
|
||||
}
|
||||
@Override public String toString() {
|
||||
return delegate + ".nonNull()";
|
||||
return delegate + ".nullSafe()";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 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;
|
||||
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();
|
||||
reader.setLenient(true);
|
||||
try {
|
||||
|
@ -178,7 +94,7 @@ public abstract class JsonAdapter<T> {
|
|||
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();
|
||||
writer.setLenient(true);
|
||||
try {
|
||||
|
@ -187,9 +103,6 @@ public abstract class JsonAdapter<T> {
|
|||
writer.setLenient(lenient);
|
||||
}
|
||||
}
|
||||
@Override boolean isLenient() {
|
||||
return true;
|
||||
}
|
||||
@Override public String toString() {
|
||||
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
|
||||
* {@linkplain JsonReader#setFailOnUnknown(boolean) unknown names and values} are encountered.
|
||||
* This constraint applies to both the top-level message handled by this type adapter as well as
|
||||
* to nested messages.
|
||||
* {@linkplain JsonReader#setFailOnUnknown(boolean) unknown values} are encountered. This
|
||||
* constraint applies to both the top-level message handled by this type adapter as well as to
|
||||
* nested messages.
|
||||
*/
|
||||
@CheckReturnValue public final JsonAdapter<T> failOnUnknown() {
|
||||
public final JsonAdapter<T> failOnUnknown() {
|
||||
final JsonAdapter<T> delegate = this;
|
||||
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();
|
||||
reader.setFailOnUnknown(true);
|
||||
try {
|
||||
|
@ -214,67 +127,24 @@ public abstract class JsonAdapter<T> {
|
|||
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);
|
||||
}
|
||||
@Override boolean isLenient() {
|
||||
return delegate.isLenient();
|
||||
}
|
||||
@Override public String toString() {
|
||||
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 {
|
||||
/**
|
||||
* Attempts to create an adapter for {@code type} annotated with {@code annotations}. This
|
||||
* returns the adapter if one was created, or null if this factory isn't capable of creating
|
||||
* such an adapter.
|
||||
*
|
||||
* <p>Implementations may use {@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.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
@Nullable JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi);
|
||||
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;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Thrown when the data in a JSON document doesn't match the data expected by the caller. For
|
||||
* example, suppose the application expects a boolean but the JSON document contains a string. When
|
||||
|
@ -24,24 +22,20 @@ import javax.annotation.Nullable;
|
|||
*
|
||||
* <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.
|
||||
*
|
||||
* <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 JsonDataException() {
|
||||
}
|
||||
|
||||
public JsonDataException(@Nullable String message) {
|
||||
public JsonDataException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public JsonDataException(@Nullable Throwable cause) {
|
||||
public JsonDataException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public JsonDataException(@Nullable String message, @Nullable Throwable cause) {
|
||||
public JsonDataException(String message, Throwable 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. */
|
||||
final class JsonScope {
|
||||
private JsonScope() {
|
||||
}
|
||||
|
||||
/** An array with no elements requires no separators or newlines before it is closed. */
|
||||
static final int EMPTY_ARRAY = 1;
|
||||
|
@ -54,7 +52,7 @@ final class JsonScope {
|
|||
*/
|
||||
static String getPath(int stackSize, int[] stack, String[] pathNames, int[] pathIndices) {
|
||||
StringBuilder result = new StringBuilder().append('$');
|
||||
for (int i = 0; i < stackSize; i++) {
|
||||
for (int i = 0, size = stackSize; i < size; i++) {
|
||||
switch (stack[i]) {
|
||||
case EMPTY_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.Flushable;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* literal values (strings, numbers, booleans and nulls) as well as the begin
|
||||
* 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
|
||||
* public void writeJsonStream(BufferedSink sink, List<Message> messages) throws IOException {
|
||||
* JsonWriter writer = JsonWriter.of(sink);
|
||||
* writer.setIndent(" ");
|
||||
* writer.setIndentSpaces(4);
|
||||
* writeMessagesArray(writer, messages);
|
||||
* 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
|
||||
* malformed JSON string will fail with an {@link IllegalStateException}.
|
||||
*/
|
||||
public abstract class JsonWriter implements Closeable, Flushable {
|
||||
// The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack will
|
||||
// grow itself up to 256 levels of nesting including the top-level document. Deeper nesting is
|
||||
// prone to trigger StackOverflowErrors.
|
||||
int stackSize = 0;
|
||||
int[] scopes = new int[32];
|
||||
String[] pathNames = new String[32];
|
||||
int[] pathIndices = new int[32];
|
||||
public class JsonWriter implements Closeable, Flushable {
|
||||
|
||||
/*
|
||||
* From RFC 4627, "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;
|
||||
|
||||
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
|
||||
* pretty printing.
|
||||
* A string containing a full set of spaces for a single level of
|
||||
* indentation, or null for no pretty printing.
|
||||
*/
|
||||
String indent;
|
||||
boolean lenient;
|
||||
boolean serializeNulls;
|
||||
boolean promoteValueToName;
|
||||
private String indent;
|
||||
|
||||
/**
|
||||
* Controls the deepest stack size that has begin/end pairs flattened:
|
||||
*
|
||||
* <ul>
|
||||
* <li>If -1, no begin/end pairs are being suppressed.
|
||||
* <li>If positive, this is the deepest stack size whose begin/end pairs are eligible to be
|
||||
* flattened.
|
||||
* <li>If negative, it is the bitwise inverse (~) of the deepest stack size whose begin/end
|
||||
* pairs have been flattened.
|
||||
* </ul>
|
||||
*
|
||||
* <p>We differentiate between what layer would be flattened (positive) from what layer is being
|
||||
* flattened (negative) so that we don't double-flatten.
|
||||
*
|
||||
* <p>To accommodate nested flattening we require callers to track the previous state when they
|
||||
* provide a new state. The previous state is returned from {@link #beginFlatten} and restored
|
||||
* with {@link #endFlatten}.
|
||||
* The name/value separator; either ":" or ": ".
|
||||
*/
|
||||
int flattenStackSize = -1;
|
||||
private String separator = ":";
|
||||
|
||||
/** Returns a new instance that writes UTF-8 encoded JSON to {@code sink}. */
|
||||
@CheckReturnValue public static JsonWriter of(BufferedSink sink) {
|
||||
return new JsonUtf8Writer(sink);
|
||||
}
|
||||
private boolean lenient;
|
||||
|
||||
JsonWriter() {
|
||||
// Package-private to control subclasses.
|
||||
}
|
||||
private String deferredName;
|
||||
|
||||
/** Returns the scope on the top of the stack. */
|
||||
final int peekScope() {
|
||||
if (stackSize == 0) {
|
||||
throw new IllegalStateException("JsonWriter is closed.");
|
||||
private boolean serializeNulls;
|
||||
|
||||
private boolean promoteNameToValue;
|
||||
|
||||
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() {
|
||||
if (stackSize != scopes.length) return false;
|
||||
|
||||
if (stackSize == 256) {
|
||||
throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?");
|
||||
}
|
||||
|
||||
scopes = Arrays.copyOf(scopes, scopes.length * 2);
|
||||
pathNames = Arrays.copyOf(pathNames, pathNames.length * 2);
|
||||
pathIndices = Arrays.copyOf(pathIndices, pathIndices.length * 2);
|
||||
if (this instanceof JsonValueWriter) {
|
||||
((JsonValueWriter) this).stack =
|
||||
Arrays.copyOf(((JsonValueWriter) this).stack, ((JsonValueWriter) this).stack.length * 2);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
final void pushScope(int newTop) {
|
||||
scopes[stackSize++] = newTop;
|
||||
}
|
||||
|
||||
/** Replace the value on the top of the stack with the given value. */
|
||||
final void replaceTop(int topOfStack) {
|
||||
scopes[stackSize - 1] = topOfStack;
|
||||
/**
|
||||
* Returns a new instance that writes a JSON-encoded stream to {@code sink}.
|
||||
*/
|
||||
public static JsonWriter of(BufferedSink sink) {
|
||||
return new JsonWriter(sink);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -215,22 +204,20 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
|||
*
|
||||
* @param indent a string containing only whitespace.
|
||||
*/
|
||||
public void setIndent(String indent) {
|
||||
this.indent = !indent.isEmpty() ? indent : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string containing only whitespace, used for each level of
|
||||
* indentation. If empty, the encoded document will be compact.
|
||||
*/
|
||||
@CheckReturnValue public final String getIndent() {
|
||||
return indent != null ? indent : "";
|
||||
public final void setIndent(String indent) {
|
||||
if (indent.length() == 0) {
|
||||
this.indent = null;
|
||||
this.separator = ":";
|
||||
} else {
|
||||
this.indent = indent;
|
||||
this.separator = ": ";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure this writer to relax its syntax rules. By default, this writer
|
||||
* 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:
|
||||
* <ul>
|
||||
* <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.
|
||||
*/
|
||||
@CheckReturnValue public final boolean isLenient() {
|
||||
public boolean isLenient() {
|
||||
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.
|
||||
* This has no impact on array elements. The default is false.
|
||||
*/
|
||||
@CheckReturnValue public final boolean getSerializeNulls() {
|
||||
public final boolean getSerializeNulls() {
|
||||
return serializeNulls;
|
||||
}
|
||||
|
||||
|
@ -272,14 +259,19 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
|||
*
|
||||
* @return this writer.
|
||||
*/
|
||||
public abstract JsonWriter beginArray() throws IOException;
|
||||
public JsonWriter beginArray() throws IOException {
|
||||
writeDeferredName();
|
||||
return open(EMPTY_ARRAY, "[");
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends encoding the current array.
|
||||
*
|
||||
* @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
|
||||
|
@ -287,22 +279,112 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
|||
*
|
||||
* @return this writer.
|
||||
*/
|
||||
public abstract JsonWriter beginObject() throws IOException;
|
||||
public JsonWriter beginObject() throws IOException {
|
||||
writeDeferredName();
|
||||
return open(EMPTY_OBJECT, "{");
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends encoding the current object.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
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}.
|
||||
|
@ -310,28 +392,52 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
|||
* @param value the literal string value, or null to encode a null literal.
|
||||
* @return this writer.
|
||||
*/
|
||||
public abstract JsonWriter value(@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}.
|
||||
*
|
||||
* @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}.
|
||||
*
|
||||
* @return this writer.
|
||||
*/
|
||||
public abstract JsonWriter value(boolean value) throws IOException;
|
||||
|
||||
/**
|
||||
* Encodes {@code value}.
|
||||
*
|
||||
* @return this writer.
|
||||
*/
|
||||
public abstract JsonWriter value(@Nullable Boolean value) throws IOException;
|
||||
public JsonWriter value(boolean value) throws IOException {
|
||||
writeDeferredName();
|
||||
beforeValue(false);
|
||||
sink.writeUtf8(value ? "true" : "false");
|
||||
pathIndices[stackSize - 1]++;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes {@code value}.
|
||||
|
@ -340,14 +446,35 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
|||
* {@linkplain Double#isInfinite() infinities}.
|
||||
* @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}.
|
||||
*
|
||||
* @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}.
|
||||
|
@ -356,116 +483,172 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
|||
* {@linkplain Double#isInfinite() infinities}.
|
||||
* @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.
|
||||
* Since no validation is performed, {@link #setSerializeNulls} and other writer configurations
|
||||
* are not respected.
|
||||
* Ensures all buffered data is written to the underlying {@link Sink}
|
||||
* and flushes that writer.
|
||||
*/
|
||||
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
|
||||
* that arbitrary type adapters can use {@link #value} to write a name value.
|
||||
* Inserts any necessary separators and whitespace before a name. Also
|
||||
* adjusts the stack to expect the name's value.
|
||||
*/
|
||||
final void promoteValueToName() throws IOException {
|
||||
int context = peekScope();
|
||||
private void beforeName() throws IOException {
|
||||
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) {
|
||||
throw new IllegalStateException("Nesting problem.");
|
||||
}
|
||||
promoteValueToName = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels immediately-nested calls to {@link #beginArray()} or {@link #beginObject()} and their
|
||||
* matching calls to {@link #endArray} or {@link #endObject()}. Use this to compose JSON adapters
|
||||
* without nesting.
|
||||
*
|
||||
* <p>For example, the following creates JSON with nested arrays: {@code [1,[2,3,4],5]}.
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* JsonAdapter<List<Integer>> integersAdapter = ...
|
||||
*
|
||||
* public void writeNumbers(JsonWriter writer) {
|
||||
* writer.beginArray();
|
||||
* writer.value(1);
|
||||
* integersAdapter.toJson(writer, Arrays.asList(2, 3, 4));
|
||||
* writer.value(5);
|
||||
* writer.endArray();
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* <p>With flattening we can create JSON with a single array {@code [1,2,3,4,5]}:
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* JsonAdapter<List<Integer>> integersAdapter = ...
|
||||
*
|
||||
* public void writeNumbers(JsonWriter writer) {
|
||||
* writer.beginArray();
|
||||
* int token = writer.beginFlatten();
|
||||
* writer.value(1);
|
||||
* integersAdapter.toJson(writer, Arrays.asList(2, 3, 4));
|
||||
* writer.value(5);
|
||||
* writer.endFlatten(token);
|
||||
* writer.endArray();
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* <p>This method flattens arrays within arrays:
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* Emit: [1, [2, 3, 4], 5]
|
||||
* To produce: [1, 2, 3, 4, 5]
|
||||
* }</pre>
|
||||
*
|
||||
* It also flattens objects within objects. Do not call {@link #name} before writing a flattened
|
||||
* object.
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* Emit: {"a": 1, {"b": 2}, "c": 3}
|
||||
* To Produce: {"a": 1, "b": 2, "c": 3}
|
||||
* }</pre>
|
||||
*
|
||||
* Other combinations are permitted but do not perform flattening. For example, objects inside of
|
||||
* arrays are not flattened:
|
||||
*
|
||||
* <pre>{@code
|
||||
*
|
||||
* Emit: [1, {"b": 2}, 3, [4, 5], 6]
|
||||
* To Produce: [1, {"b": 2}, 3, 4, 5, 6]
|
||||
* }</pre>
|
||||
*
|
||||
* <p>This method returns an opaque token. Callers must match all calls to this method with a call
|
||||
* to {@link #endFlatten} with the matching token.
|
||||
*/
|
||||
@CheckReturnValue public final int beginFlatten() {
|
||||
int context = peekScope();
|
||||
if (context != NONEMPTY_OBJECT && context != EMPTY_OBJECT
|
||||
&& context != NONEMPTY_ARRAY && context != EMPTY_ARRAY) {
|
||||
throw new IllegalStateException("Nesting problem.");
|
||||
}
|
||||
int token = flattenStackSize;
|
||||
flattenStackSize = stackSize;
|
||||
return token;
|
||||
}
|
||||
|
||||
/** Ends nested call flattening created by {@link #beginFlatten}. */
|
||||
public final void endFlatten(int token) {
|
||||
flattenStackSize = token;
|
||||
promoteNameToValue = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a <a href="http://goessner.net/articles/JsonPath/">JsonPath</a> to
|
||||
* the current location in the JSON value.
|
||||
*/
|
||||
@CheckReturnValue public final String getPath() {
|
||||
return JsonScope.getPath(stackSize, scopes, pathNames, pathIndices);
|
||||
public String getPath() {
|
||||
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
|
||||
* comparable and non-null.
|
||||
*/
|
||||
LinkedHashTreeMap() {
|
||||
public LinkedHashTreeMap() {
|
||||
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
|
||||
* use the natural ordering.
|
||||
*/
|
||||
@SuppressWarnings({
|
||||
"unchecked", "rawtypes" // Unsafe! if comparator is null, this assumes K is comparable.
|
||||
})
|
||||
LinkedHashTreeMap(Comparator<? super K> comparator) {
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" }) // unsafe! if comparator is null, this assumes K is comparable
|
||||
public LinkedHashTreeMap(Comparator<? super K> comparator) {
|
||||
this.comparator = comparator != null
|
||||
? comparator
|
||||
: (Comparator) NATURAL_ORDER;
|
||||
|
@ -475,14 +473,14 @@ final class LinkedHashTreeMap<K, V> extends AbstractMap<K, V> implements Seriali
|
|||
V value;
|
||||
int height;
|
||||
|
||||
/** Create the header entry. */
|
||||
/** Create the header entry */
|
||||
Node() {
|
||||
key = null;
|
||||
hash = -1;
|
||||
next = prev = this;
|
||||
}
|
||||
|
||||
/** Create a regular entry. */
|
||||
/** Create a regular entry */
|
||||
Node(Node<K, V> parent, K key, int hash, Node<K, V> next, Node<K, V> prev) {
|
||||
this.parent = parent;
|
||||
this.key = key;
|
||||
|
@ -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
|
||||
* {@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. */
|
||||
private Node<K, V> stack;
|
||||
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> lastReturned = null;
|
||||
int expectedModCount = modCount;
|
||||
|
|
|
@ -20,7 +20,6 @@ import java.lang.annotation.Annotation;
|
|||
import java.lang.reflect.Type;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Converts maps with string keys to JSON objects.
|
||||
|
@ -29,7 +28,7 @@ import javax.annotation.Nullable;
|
|||
*/
|
||||
final class MapJsonAdapter<K, V> extends JsonAdapter<Map<K, V>> {
|
||||
public static final Factory FACTORY = new Factory() {
|
||||
@Override public @Nullable JsonAdapter<?> create(
|
||||
@Override public JsonAdapter<?> create(
|
||||
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
if (!annotations.isEmpty()) return null;
|
||||
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<V> valueAdapter;
|
||||
|
||||
MapJsonAdapter(Moshi moshi, Type keyType, Type valueType) {
|
||||
public MapJsonAdapter(Moshi moshi, Type keyType, Type valueType) {
|
||||
this.keyAdapter = moshi.adapter(keyType);
|
||||
this.valueAdapter = moshi.adapter(valueType);
|
||||
}
|
||||
|
@ -51,9 +50,9 @@ final class MapJsonAdapter<K, V> extends JsonAdapter<Map<K, V>> {
|
|||
writer.beginObject();
|
||||
for (Map.Entry<K, V> entry : map.entrySet()) {
|
||||
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());
|
||||
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);
|
||||
if (replaced != null) {
|
||||
throw new JsonDataException("Map key '" + name + "' has multiple values at path "
|
||||
+ reader.getPath() + ": " + replaced + " and " + value);
|
||||
+ reader.getPath());
|
||||
}
|
||||
}
|
||||
reader.endObject();
|
||||
|
|
|
@ -15,106 +15,47 @@
|
|||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import com.squareup.moshi.internal.Util;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static com.squareup.moshi.internal.Util.canonicalize;
|
||||
import static com.squareup.moshi.internal.Util.removeSubtypeWildcard;
|
||||
import static com.squareup.moshi.internal.Util.typeAnnotatedWithAnnotations;
|
||||
|
||||
/**
|
||||
* Coordinates binding between JSON values and Java objects.
|
||||
*/
|
||||
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 ThreadLocal<LookupChain> lookupChainThreadLocal = new ThreadLocal<>();
|
||||
private final ThreadLocal<List<DeferredAdapter<?>>> reentrantCalls = new ThreadLocal<>();
|
||||
private final Map<Object, JsonAdapter<?>> adapterCache = new LinkedHashMap<>();
|
||||
|
||||
Moshi(Builder builder) {
|
||||
List<JsonAdapter.Factory> factories = new ArrayList<>(
|
||||
builder.factories.size() + BUILT_IN_FACTORIES.size());
|
||||
private Moshi(Builder builder) {
|
||||
List<JsonAdapter.Factory> factories = new ArrayList<>();
|
||||
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);
|
||||
}
|
||||
|
||||
/** 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);
|
||||
}
|
||||
|
||||
@CheckReturnValue public <T> JsonAdapter<T> adapter(Class<T> type) {
|
||||
public <T> JsonAdapter<T> adapter(Class<T> type) {
|
||||
return adapter(type, Util.NO_ANNOTATIONS);
|
||||
}
|
||||
|
||||
@CheckReturnValue
|
||||
public <T> JsonAdapter<T> adapter(Type type, Class<? extends Annotation> annotationType) {
|
||||
if (annotationType == null) {
|
||||
throw new NullPointerException("annotationType == null");
|
||||
}
|
||||
return adapter(type,
|
||||
Collections.singleton(Types.createJsonQualifierImplementation(annotationType)));
|
||||
}
|
||||
|
||||
@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.
|
||||
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));
|
||||
|
||||
public <T> JsonAdapter<T> adapter(Type type, Set<? extends Annotation> annotations) {
|
||||
// If there's an equivalent adapter in the cache, we're done!
|
||||
Object cacheKey = cacheKey(type, annotations);
|
||||
synchronized (adapterCache) {
|
||||
|
@ -122,45 +63,47 @@ public final class Moshi {
|
|||
if (result != null) return (JsonAdapter<T>) result;
|
||||
}
|
||||
|
||||
LookupChain lookupChain = lookupChainThreadLocal.get();
|
||||
if (lookupChain == null) {
|
||||
lookupChain = new LookupChain();
|
||||
lookupChainThreadLocal.set(lookupChain);
|
||||
// Short-circuit if this is a reentrant call.
|
||||
List<DeferredAdapter<?>> deferredAdapters = reentrantCalls.get();
|
||||
if (deferredAdapters != null) {
|
||||
for (int i = 0, size = deferredAdapters.size(); i < size; i++) {
|
||||
DeferredAdapter<?> deferredAdapter = deferredAdapters.get(i);
|
||||
if (deferredAdapter.cacheKey.equals(cacheKey)) {
|
||||
return (JsonAdapter<T>) deferredAdapter;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
deferredAdapters = new ArrayList<>();
|
||||
reentrantCalls.set(deferredAdapters);
|
||||
}
|
||||
|
||||
boolean success = false;
|
||||
JsonAdapter<T> adapterFromCall = lookupChain.push(type, fieldName, cacheKey);
|
||||
// Prepare for re-entrant calls, then ask each factory to create a type adapter.
|
||||
DeferredAdapter<T> deferredAdapter = new DeferredAdapter<>(cacheKey);
|
||||
deferredAdapters.add(deferredAdapter);
|
||||
try {
|
||||
if (adapterFromCall != null) return adapterFromCall;
|
||||
|
||||
// Ask each factory to create the JSON adapter.
|
||||
for (int i = 0, size = factories.size(); i < size; i++) {
|
||||
JsonAdapter<T> result = (JsonAdapter<T>) factories.get(i).create(type, annotations, this);
|
||||
if (result == null) continue;
|
||||
|
||||
// Success! Notify the LookupChain so it is cached and can be used by re-entrant calls.
|
||||
lookupChain.adapterFound(result);
|
||||
success = true;
|
||||
return result;
|
||||
if (result != null) {
|
||||
deferredAdapter.ready(result);
|
||||
synchronized (adapterCache) {
|
||||
adapterCache.put(cacheKey, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(
|
||||
"No JsonAdapter for " + typeAnnotatedWithAnnotations(type, annotations));
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw lookupChain.exceptionWithLookupStack(e);
|
||||
} 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.
|
||||
public <T> JsonAdapter<T> nextAdapter(JsonAdapter.Factory skipPast, Type type,
|
||||
Set<? extends Annotation> annotations) {
|
||||
if (annotations == null) throw new NullPointerException("annotations == null");
|
||||
|
||||
type = removeSubtypeWildcard(canonicalize(type));
|
||||
|
||||
int skipPastIndex = factories.indexOf(skipPast);
|
||||
if (skipPastIndex == -1) {
|
||||
throw new IllegalArgumentException("Unable to skip past unknown factory " + skipPast);
|
||||
|
@ -170,15 +113,7 @@ public final class Moshi {
|
|||
if (result != null) return result;
|
||||
}
|
||||
throw new IllegalArgumentException("No next JsonAdapter for "
|
||||
+ typeAnnotatedWithAnnotations(type, 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);
|
||||
+ type + " annotated " + annotations);
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
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) {
|
||||
if (type == null) throw new IllegalArgumentException("type == null");
|
||||
if (jsonAdapter == null) throw new IllegalArgumentException("jsonAdapter == null");
|
||||
|
||||
return add(new JsonAdapter.Factory() {
|
||||
@Override public @Nullable JsonAdapter<?> create(
|
||||
@Override public JsonAdapter<?> create(
|
||||
Type targetType, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
return annotations.isEmpty() && Util.typesMatch(type, targetType) ? jsonAdapter : null;
|
||||
}
|
||||
|
@ -215,7 +150,7 @@ public final class Moshi {
|
|||
}
|
||||
|
||||
return add(new JsonAdapter.Factory() {
|
||||
@Override public @Nullable JsonAdapter<?> create(
|
||||
@Override public JsonAdapter<?> create(
|
||||
Type targetType, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
if (Util.typesMatch(type, targetType)
|
||||
&& annotations.size() == 1
|
||||
|
@ -227,154 +162,49 @@ public final class Moshi {
|
|||
});
|
||||
}
|
||||
|
||||
public Builder add(JsonAdapter.Factory factory) {
|
||||
if (factory == null) throw new IllegalArgumentException("factory == null");
|
||||
factories.add(factory);
|
||||
public Builder add(JsonAdapter.Factory jsonAdapter) {
|
||||
factories.add(jsonAdapter);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder add(Object adapter) {
|
||||
if (adapter == null) throw new IllegalArgumentException("adapter == null");
|
||||
return add(AdapterMethodsFactory.get(adapter));
|
||||
}
|
||||
|
||||
Builder addAll(List<JsonAdapter.Factory> factories) {
|
||||
this.factories.addAll(factories);
|
||||
return this;
|
||||
}
|
||||
|
||||
@CheckReturnValue public Moshi build() {
|
||||
public Moshi build() {
|
||||
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
|
||||
* for Employee, re-enter looking for the JSON adapter of HomeAddress, and re-enter again looking
|
||||
* up the JSON adapter of PostalCode. If any of these lookups fail we can provide a stack trace
|
||||
* with all of the lookups.
|
||||
*
|
||||
* <p>Sometimes a JSON adapter factory depends on its own product; either directly or indirectly.
|
||||
* To make this work, we offer a JSON adapter stub while the final adapter is being computed.
|
||||
* When it is ready, we wire the stub to that finished adapter. This is necessary in
|
||||
* self-referential object models, such as an {@code Employee} class that has a {@code
|
||||
* List<Employee>} field for an organization's management hierarchy.
|
||||
*
|
||||
* <p>This class defers putting any JSON adapters in the cache until the topmost JSON adapter has
|
||||
* successfully been computed. That way we don't pollute the cache with incomplete stubs, or
|
||||
* adapters that may transitively depend on incomplete stubs.
|
||||
* <p>Typically this is necessary in self-referential object models, such as an {@code Employee}
|
||||
* class that has a {@code List<Employee>} field for an organization's management hierarchy.
|
||||
*/
|
||||
final class LookupChain {
|
||||
final List<Lookup<?>> callLookups = new ArrayList<>();
|
||||
final Deque<Lookup<?>> stack = new ArrayDeque<>();
|
||||
boolean exceptionAnnotated;
|
||||
private static class DeferredAdapter<T> extends JsonAdapter<T> {
|
||||
private Object cacheKey;
|
||||
private JsonAdapter<T> delegate;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
public DeferredAdapter(Object cacheKey) {
|
||||
this.cacheKey = cacheKey;
|
||||
}
|
||||
|
||||
public void ready(JsonAdapter<T> delegate) {
|
||||
this.delegate = delegate;
|
||||
this.cacheKey = null;
|
||||
}
|
||||
|
||||
@Override public T fromJson(JsonReader reader) throws IOException {
|
||||
if (adapter == null) throw new IllegalStateException("JsonAdapter isn't ready");
|
||||
return adapter.fromJson(reader);
|
||||
if (delegate == null) throw new IllegalStateException("Type adapter isn't ready");
|
||||
return delegate.fromJson(reader);
|
||||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, T value) throws IOException {
|
||||
if (adapter == null) throw new IllegalStateException("JsonAdapter isn't ready");
|
||||
adapter.toJson(writer, value);
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
return adapter != null ? adapter.toString() : super.toString();
|
||||
if (delegate == null) throw new IllegalStateException("Type adapter isn't ready");
|
||||
delegate.toJson(writer, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,23 +15,17 @@
|
|||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import com.squareup.moshi.internal.Util;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static com.squareup.moshi.internal.Util.generatedAdapter;
|
||||
|
||||
final class StandardJsonAdapters {
|
||||
private StandardJsonAdapters() {
|
||||
}
|
||||
|
||||
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
|
||||
@Override public JsonAdapter<?> create(
|
||||
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
|
||||
|
@ -56,15 +50,9 @@ final class StandardJsonAdapters {
|
|||
if (type == Object.class) return new ObjectJsonAdapter(moshi).nullSafe();
|
||||
|
||||
Class<?> rawType = Types.getRawType(type);
|
||||
|
||||
@Nullable JsonAdapter<?> generatedAdapter = generatedAdapter(moshi, type, rawType);
|
||||
if (generatedAdapter != null) {
|
||||
return generatedAdapter;
|
||||
}
|
||||
|
||||
if (rawType.isEnum()) {
|
||||
//noinspection unchecked
|
||||
return new EnumJsonAdapter<>((Class<? extends Enum>) rawType).nullSafe();
|
||||
return enumAdapter((Class<? extends Enum>) rawType).nullSafe();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -72,7 +60,7 @@ final class StandardJsonAdapters {
|
|||
|
||||
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 {
|
||||
int value = reader.nextInt();
|
||||
if (value < min || value > max) {
|
||||
|
@ -88,7 +76,7 @@ final class StandardJsonAdapters {
|
|||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, Boolean value) throws IOException {
|
||||
writer.value(value.booleanValue());
|
||||
writer.value(value);
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
|
@ -224,47 +212,27 @@ final class StandardJsonAdapters {
|
|||
}
|
||||
};
|
||||
|
||||
static final class EnumJsonAdapter<T extends Enum<T>> extends JsonAdapter<T> {
|
||||
private final Class<T> enumType;
|
||||
private final String[] nameStrings;
|
||||
private final T[] constants;
|
||||
private final JsonReader.Options options;
|
||||
|
||||
EnumJsonAdapter(Class<T> enumType) {
|
||||
this.enumType = enumType;
|
||||
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;
|
||||
static <T extends Enum<T>> JsonAdapter<T> enumAdapter(final Class<T> enumType) {
|
||||
return new JsonAdapter<T>() {
|
||||
@Override public T fromJson(JsonReader reader) throws IOException {
|
||||
String name = reader.nextString();
|
||||
try {
|
||||
return Enum.valueOf(enumType, name);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new JsonDataException("Expected one of "
|
||||
+ Arrays.toString(enumType.getEnumConstants()) + " but was " + name + " at path "
|
||||
+ reader.getPath());
|
||||
}
|
||||
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 {
|
||||
int index = reader.selectString(options);
|
||||
if (index != -1) return constants[index];
|
||||
@Override public void toJson(JsonWriter writer, T value) throws IOException {
|
||||
writer.value(value.name());
|
||||
}
|
||||
|
||||
// We can consume the string safely, we are terminating anyway.
|
||||
String path = reader.getPath();
|
||||
String name = reader.nextString();
|
||||
throw new JsonDataException("Expected one of "
|
||||
+ Arrays.asList(nameStrings) + " but was " + name + " at path " + path);
|
||||
}
|
||||
|
||||
@Override public void toJson(JsonWriter writer, T value) throws IOException {
|
||||
writer.value(nameStrings[value.ordinal()]);
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
return "JsonAdapter(" + enumType.getName() + ")";
|
||||
}
|
||||
@Override public String toString() {
|
||||
return "JsonAdapter(" + enumType.getName() + ")";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -277,44 +245,46 @@ final class StandardJsonAdapters {
|
|||
*/
|
||||
static final class ObjectJsonAdapter extends JsonAdapter<Object> {
|
||||
private final Moshi moshi;
|
||||
private final JsonAdapter<List> listJsonAdapter;
|
||||
private final JsonAdapter<Map> mapAdapter;
|
||||
private final JsonAdapter<String> stringAdapter;
|
||||
private final JsonAdapter<Double> doubleAdapter;
|
||||
private final JsonAdapter<Boolean> booleanAdapter;
|
||||
|
||||
ObjectJsonAdapter(Moshi moshi) {
|
||||
public ObjectJsonAdapter(Moshi moshi) {
|
||||
this.moshi = moshi;
|
||||
this.listJsonAdapter = moshi.adapter(List.class);
|
||||
this.mapAdapter = moshi.adapter(Map.class);
|
||||
this.stringAdapter = moshi.adapter(String.class);
|
||||
this.doubleAdapter = moshi.adapter(Double.class);
|
||||
this.booleanAdapter = moshi.adapter(Boolean.class);
|
||||
}
|
||||
|
||||
@Override public Object fromJson(JsonReader reader) throws IOException {
|
||||
switch (reader.peek()) {
|
||||
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:
|
||||
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:
|
||||
return stringAdapter.fromJson(reader);
|
||||
return reader.nextString();
|
||||
|
||||
case NUMBER:
|
||||
return doubleAdapter.fromJson(reader);
|
||||
return reader.nextDouble();
|
||||
|
||||
case BOOLEAN:
|
||||
return booleanAdapter.fromJson(reader);
|
||||
return reader.nextBoolean();
|
||||
|
||||
case NULL:
|
||||
return reader.nextNull();
|
||||
|
||||
default:
|
||||
throw new IllegalStateException(
|
||||
"Expected a value but was " + reader.peek() + " at path " + reader.getPath());
|
||||
throw new IllegalStateException("Expected a value but was " + reader.peek()
|
||||
+ " at path " + reader.getPath());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,112 +15,34 @@
|
|||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import com.squareup.moshi.internal.Util.GenericArrayTypeImpl;
|
||||
import com.squareup.moshi.internal.Util.ParameterizedTypeImpl;
|
||||
import com.squareup.moshi.internal.Util.WildcardTypeImpl;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.GenericArrayType;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.GenericDeclaration;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.lang.reflect.Type;
|
||||
import java.lang.reflect.TypeVariable;
|
||||
import java.lang.reflect.WildcardType;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static com.squareup.moshi.internal.Util.EMPTY_TYPE_ARRAY;
|
||||
import static com.squareup.moshi.internal.Util.getGenericSupertype;
|
||||
import static com.squareup.moshi.internal.Util.resolve;
|
||||
|
||||
/** Factory methods for types. */
|
||||
@CheckReturnValue
|
||||
public final class Types {
|
||||
static final Type[] EMPTY_TYPE_ARRAY = new Type[] {};
|
||||
|
||||
private Types() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the generated {@link JsonAdapter} fully qualified class name for a given
|
||||
* {@link JsonClass JsonClass-annotated} {@code clazz}. This is the same lookup logic used by
|
||||
* both the Moshi code generation as well as lookup for any JsonClass-annotated classes. This can
|
||||
* be useful if generating your own JsonAdapters without using Moshi's first party code gen.
|
||||
*
|
||||
* @param clazz the class to calculate a generated JsonAdapter name for.
|
||||
* @return the resolved fully qualified class name to the expected generated JsonAdapter class.
|
||||
* Note that this name will always be a top-level class name and not a nested class.
|
||||
*/
|
||||
public static String generatedJsonAdapterName(Class<?> clazz) {
|
||||
if (clazz.getAnnotation(JsonClass.class) == null) {
|
||||
throw new IllegalArgumentException("Class does not have a JsonClass annotation: " + clazz);
|
||||
}
|
||||
return generatedJsonAdapterName(clazz.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the generated {@link JsonAdapter} fully qualified class name for a given
|
||||
* {@link JsonClass JsonClass-annotated} {@code className}. This is the same lookup logic used by
|
||||
* both the Moshi code generation as well as lookup for any JsonClass-annotated classes. This can
|
||||
* be useful if generating your own JsonAdapters without using Moshi's first party code gen.
|
||||
*
|
||||
* @param className the fully qualified class to calculate a generated JsonAdapter name for.
|
||||
* @return the resolved fully qualified class name to the expected generated JsonAdapter class.
|
||||
* Note that this name will always be a top-level class name and not a nested class.
|
||||
*/
|
||||
public static String generatedJsonAdapterName(String className) {
|
||||
return className.replace("$", "_") + "JsonAdapter";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if {@code annotations} contains {@code jsonQualifier}.
|
||||
* Returns the subset of {@code annotations} without {@code jsonQualifier}, or null if {@code
|
||||
* annotations} does not contain {@code jsonQualifier}.
|
||||
*/
|
||||
public static @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.
|
||||
* Returns a new parameterized type, applying {@code typeArguments} to {@code rawType}.
|
||||
*/
|
||||
public static ParameterizedType newParameterizedType(Type rawType, Type... 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}. */
|
||||
public static GenericArrayType arrayOf(Type componentType) {
|
||||
return new GenericArrayTypeImpl(componentType);
|
||||
|
@ -144,6 +66,33 @@ public final class Types {
|
|||
return new WildcardTypeImpl(new Type[] { Object.class }, new Type[] { bound });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a type that is functionally equal but not necessarily equal according to {@link
|
||||
* Object#equals(Object) Object.equals()}.
|
||||
*/
|
||||
static Type canonicalize(Type type) {
|
||||
if (type instanceof Class) {
|
||||
Class<?> c = (Class<?>) type;
|
||||
return c.isArray() ? new GenericArrayTypeImpl(canonicalize(c.getComponentType())) : c;
|
||||
|
||||
} else if (type instanceof ParameterizedType) {
|
||||
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) {
|
||||
if (type instanceof Class<?>) {
|
||||
// type is a normal class.
|
||||
|
@ -158,7 +107,7 @@ public final class Types {
|
|||
return (Class<?>) rawType;
|
||||
|
||||
} else if (type instanceof GenericArrayType) {
|
||||
Type componentType = ((GenericArrayType) type).getGenericComponentType();
|
||||
Type componentType = ((GenericArrayType)type).getGenericComponentType();
|
||||
return Array.newInstance(getRawType(componentType), 0).getClass();
|
||||
|
||||
} else if (type instanceof TypeVariable) {
|
||||
|
@ -176,32 +125,16 @@ public final class Types {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the element type of this collection type.
|
||||
* @throws IllegalArgumentException if this type is not a collection.
|
||||
*/
|
||||
public static Type collectionElementType(Type context, Class<?> contextRawType) {
|
||||
Type collectionType = getSupertype(context, contextRawType, Collection.class);
|
||||
|
||||
if (collectionType instanceof WildcardType) {
|
||||
collectionType = ((WildcardType) collectionType).getUpperBounds()[0];
|
||||
}
|
||||
if (collectionType instanceof ParameterizedType) {
|
||||
return ((ParameterizedType) collectionType).getActualTypeArguments()[0];
|
||||
}
|
||||
return Object.class;
|
||||
static boolean equal(Object a, Object b) {
|
||||
return a == b || (a != null && a.equals(b));
|
||||
}
|
||||
|
||||
/** Returns true if {@code a} and {@code b} are equal. */
|
||||
public static boolean equals(@Nullable Type a, @Nullable Type b) {
|
||||
static boolean equals(Type a, Type b) {
|
||||
if (a == b) {
|
||||
return true; // Also handles (a == null && b == null).
|
||||
|
||||
} else if (a instanceof Class) {
|
||||
if (b instanceof GenericArrayType) {
|
||||
return equals(((Class) a).getComponentType(),
|
||||
((GenericArrayType) b).getGenericComponentType());
|
||||
}
|
||||
return a.equals(b); // Class already specifies equals().
|
||||
|
||||
} else if (a instanceof ParameterizedType) {
|
||||
|
@ -214,15 +147,11 @@ public final class Types {
|
|||
Type[] bTypeArguments = pb instanceof ParameterizedTypeImpl
|
||||
? ((ParameterizedTypeImpl) pb).typeArguments
|
||||
: pb.getActualTypeArguments();
|
||||
return equals(pa.getOwnerType(), pb.getOwnerType())
|
||||
return equal(pa.getOwnerType(), pb.getOwnerType())
|
||||
&& pa.getRawType().equals(pb.getRawType())
|
||||
&& Arrays.equals(aTypeArguments, bTypeArguments);
|
||||
|
||||
} else if (a instanceof GenericArrayType) {
|
||||
if (b instanceof Class) {
|
||||
return equals(((Class) b).getComponentType(),
|
||||
((GenericArrayType) a).getGenericComponentType());
|
||||
}
|
||||
if (!(b instanceof GenericArrayType)) return false;
|
||||
GenericArrayType ga = (GenericArrayType) a;
|
||||
GenericArrayType gb = (GenericArrayType) b;
|
||||
|
@ -243,87 +172,56 @@ public final class Types {
|
|||
&& va.getName().equals(vb.getName());
|
||||
|
||||
} else {
|
||||
// This isn't a supported type.
|
||||
// This isn't a supported type. Could be a generic array type, wildcard type, etc.
|
||||
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.
|
||||
* @param fieldName the target field name on {@code clazz}.
|
||||
* @return a set of {@link JsonQualifier}-annotated {@link Annotation} instances retrieved from
|
||||
* the targeted field. Can be empty if none are found.
|
||||
* 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 Set<? extends Annotation> getFieldJsonQualifierAnnotations(Class<?> clazz,
|
||||
String fieldName) {
|
||||
try {
|
||||
Field field = clazz.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
Annotation[] fieldAnnotations = field.getDeclaredAnnotations();
|
||||
Set<Annotation> annotations = new LinkedHashSet<>(fieldAnnotations.length);
|
||||
for (Annotation annotation : fieldAnnotations) {
|
||||
if (annotation.annotationType().isAnnotationPresent(JsonQualifier.class)) {
|
||||
annotations.add(annotation);
|
||||
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);
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableSet(annotations);
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new IllegalArgumentException("Could not access field "
|
||||
+ fieldName
|
||||
+ " on class "
|
||||
+ clazz.getCanonicalName(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T extends Annotation> T createJsonQualifierImplementation(final Class<T> annotationType) {
|
||||
if (!annotationType.isAnnotation()) {
|
||||
throw new IllegalArgumentException(annotationType + " must be an annotation.");
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 };
|
||||
// we can't resolve this further
|
||||
return toResolve;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -357,4 +255,290 @@ public final class Types {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the element type of this collection type.
|
||||
* @throws IllegalArgumentException if this type is not a collection.
|
||||
*/
|
||||
public static Type collectionElementType(Type context, Class<?> contextRawType) {
|
||||
Type collectionType = getSupertype(context, contextRawType, Collection.class);
|
||||
|
||||
if (collectionType instanceof WildcardType) {
|
||||
collectionType = ((WildcardType) collectionType).getUpperBounds()[0];
|
||||
}
|
||||
if (collectionType instanceof ParameterizedType) {
|
||||
return ((ParameterizedType) collectionType).getActualTypeArguments()[0];
|
||||
}
|
||||
return Object.class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a two element array containing this map's key and value types in positions 0 and 1
|
||||
* respectively.
|
||||
*/
|
||||
static Type[] mapKeyAndValueTypes(Type context, Class<?> contextRawType) {
|
||||
// Work around a problem with the declaration of java.util.Properties. That class should extend
|
||||
// Hashtable<String, String>, but it's declared to extend Hashtable<Object, Object>.
|
||||
if (context == Properties.class) return new Type[] { String.class, String.class };
|
||||
|
||||
Type mapType = getSupertype(context, contextRawType, Map.class);
|
||||
if (mapType instanceof ParameterizedType) {
|
||||
ParameterizedType mapParameterizedType = (ParameterizedType) mapType;
|
||||
return mapParameterizedType.getActualTypeArguments();
|
||||
}
|
||||
return new Type[] { Object.class, Object.class };
|
||||
}
|
||||
|
||||
static Type resolve(Type context, Class<?> contextRawType, Type toResolve) {
|
||||
// This implementation is made a little more complicated in an attempt to avoid object-creation.
|
||||
while (true) {
|
||||
if (toResolve instanceof TypeVariable) {
|
||||
TypeVariable<?> typeVariable = (TypeVariable<?>) toResolve;
|
||||
toResolve = resolveTypeVariable(context, contextRawType, typeVariable);
|
||||
if (toResolve == typeVariable) return toResolve;
|
||||
|
||||
} else if (toResolve instanceof Class && ((Class<?>) toResolve).isArray()) {
|
||||
Class<?> original = (Class<?>) toResolve;
|
||||
Type componentType = original.getComponentType();
|
||||
Type newComponentType = resolve(context, contextRawType, componentType);
|
||||
return componentType == newComponentType
|
||||
? original
|
||||
: arrayOf(newComponentType);
|
||||
|
||||
} else if (toResolve instanceof GenericArrayType) {
|
||||
GenericArrayType original = (GenericArrayType) toResolve;
|
||||
Type componentType = original.getGenericComponentType();
|
||||
Type newComponentType = resolve(context, contextRawType, componentType);
|
||||
return componentType == newComponentType
|
||||
? original
|
||||
: arrayOf(newComponentType);
|
||||
|
||||
} else if (toResolve instanceof ParameterizedType) {
|
||||
ParameterizedType original = (ParameterizedType) toResolve;
|
||||
Type ownerType = original.getOwnerType();
|
||||
Type newOwnerType = resolve(context, contextRawType, ownerType);
|
||||
boolean changed = newOwnerType != ownerType;
|
||||
|
||||
Type[] args = original.getActualTypeArguments();
|
||||
for (int t = 0, length = args.length; t < length; t++) {
|
||||
Type resolvedTypeArgument = resolve(context, contextRawType, args[t]);
|
||||
if (resolvedTypeArgument != args[t]) {
|
||||
if (!changed) {
|
||||
args = args.clone();
|
||||
changed = true;
|
||||
}
|
||||
args[t] = resolvedTypeArgument;
|
||||
}
|
||||
}
|
||||
|
||||
return changed
|
||||
? new ParameterizedTypeImpl(newOwnerType, original.getRawType(), args)
|
||||
: original;
|
||||
|
||||
} else if (toResolve instanceof WildcardType) {
|
||||
WildcardType original = (WildcardType) toResolve;
|
||||
Type[] originalLowerBound = original.getLowerBounds();
|
||||
Type[] originalUpperBound = original.getUpperBounds();
|
||||
|
||||
if (originalLowerBound.length == 1) {
|
||||
Type lowerBound = resolve(context, contextRawType, originalLowerBound[0]);
|
||||
if (lowerBound != originalLowerBound[0]) {
|
||||
return supertypeOf(lowerBound);
|
||||
}
|
||||
} else if (originalUpperBound.length == 1) {
|
||||
Type upperBound = resolve(context, contextRawType, originalUpperBound[0]);
|
||||
if (upperBound != originalUpperBound[0]) {
|
||||
return subtypeOf(upperBound);
|
||||
}
|
||||
}
|
||||
return original;
|
||||
|
||||
} else {
|
||||
return toResolve;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Type resolveTypeVariable(Type context, Class<?> contextRawType, TypeVariable<?> unknown) {
|
||||
Class<?> declaredByRaw = declaringClassOf(unknown);
|
||||
|
||||
// We can't reduce this further.
|
||||
if (declaredByRaw == null) return unknown;
|
||||
|
||||
Type declaredBy = getGenericSupertype(context, contextRawType, declaredByRaw);
|
||||
if (declaredBy instanceof ParameterizedType) {
|
||||
int index = indexOf(declaredByRaw.getTypeParameters(), unknown);
|
||||
return ((ParameterizedType) declaredBy).getActualTypeArguments()[index];
|
||||
}
|
||||
|
||||
return unknown;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
import com.squareup.moshi.MoshiTest.Uppercase;
|
||||
import com.squareup.moshi.MoshiTest.UppercaseAdapterFactory;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import okio.ByteString;
|
||||
import org.junit.Test;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
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 {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.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
|
||||
* @Nullable}. (The specific annotation class doesn't matter; just its simple name.)
|
||||
|
@ -387,33 +233,6 @@ public final class AdapterMethodsTest {
|
|||
@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 {
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(new ExceptionThrowingPointJsonAdapter())
|
||||
|
@ -437,12 +256,10 @@ public final class AdapterMethodsTest {
|
|||
|
||||
static class ExceptionThrowingPointJsonAdapter {
|
||||
@ToJson void pointToJson(JsonWriter writer, Point point) throws Exception {
|
||||
if (point != null) throw new Exception("pointToJson fail!");
|
||||
writer.nullValue();
|
||||
throw new Exception("pointToJson fail!");
|
||||
}
|
||||
|
||||
@FromJson Point pointFromJson(JsonReader reader) throws Exception {
|
||||
if (reader.peek() == JsonReader.Token.NULL) return reader.nextNull();
|
||||
throw new Exception("pointFromJson fail!");
|
||||
}
|
||||
}
|
||||
|
@ -462,10 +279,7 @@ public final class AdapterMethodsTest {
|
|||
fail();
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertThat(e).hasMessage("No @FromJson adapter for interface "
|
||||
+ "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)");
|
||||
assertThat(e).hasCauseExactlyInstanceOf(IllegalArgumentException.class);
|
||||
assertThat(e.getCause()).hasMessage("No next JsonAdapter for interface "
|
||||
+ "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)");
|
||||
+ "com.squareup.moshi.AdapterMethodsTest$Shape annotated []");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -484,284 +298,7 @@ public final class AdapterMethodsTest {
|
|||
fail();
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertThat(e).hasMessage("No @ToJson adapter for interface "
|
||||
+ "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)");
|
||||
assertThat(e).hasCauseExactlyInstanceOf(IllegalArgumentException.class);
|
||||
assertThat(e.getCause()).hasMessage("No next JsonAdapter for interface "
|
||||
+ "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
+ "com.squareup.moshi.AdapterMethodsTest$Shape annotated []");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
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;
|
||||
|
||||
import com.squareup.moshi.internal.Util;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.Retention;
|
||||
|
|
|
@ -25,7 +25,7 @@ import okio.Buffer;
|
|||
import org.junit.Test;
|
||||
|
||||
import static com.squareup.moshi.TestUtil.newReader;
|
||||
import static com.squareup.moshi.internal.Util.NO_ANNOTATIONS;
|
||||
import static com.squareup.moshi.Util.NO_ANNOTATIONS;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
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 {
|
||||
assertThat(ClassJsonAdapter.FACTORY.create(Interface.class, NO_ANNOTATIONS, moshi)).isNull();
|
||||
assertThat(ClassJsonAdapter.FACTORY.create(Runnable.class, NO_ANNOTATIONS, moshi)).isNull();
|
||||
}
|
||||
|
||||
static abstract class Abstract {
|
||||
|
@ -444,23 +429,6 @@ public final class ClassJsonAdapterTest {
|
|||
assertThat(fromJson.zipCode).isEqualTo("94043");
|
||||
}
|
||||
|
||||
static final class Box<T> {
|
||||
final T data;
|
||||
|
||||
Box(T data) {
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
@Test public void parameterizedType() throws Exception {
|
||||
@SuppressWarnings("unchecked")
|
||||
JsonAdapter<Box<Integer>> adapter = (JsonAdapter<Box<Integer>>) ClassJsonAdapter.FACTORY.create(
|
||||
Types.newParameterizedTypeWithOwner(ClassJsonAdapterTest.class, Box.class, Integer.class),
|
||||
NO_ANNOTATIONS, moshi);
|
||||
assertThat(adapter.fromJson("{\"data\":5}").data).isEqualTo(5);
|
||||
assertThat(adapter.toJson(new Box<>(5))).isEqualTo("{\"data\":5}");
|
||||
}
|
||||
|
||||
private <T> String toJson(Class<T> type, T value) throws IOException {
|
||||
@SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument.
|
||||
JsonAdapter<T> jsonAdapter = (JsonAdapter<T>) ClassJsonAdapter.FACTORY.create(
|
||||
|
|
|
@ -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