Compare commits

..

6 commits

Author SHA1 Message Date
Jesse Wilson
e42bb8a0f6 Revert "Add example for custom JsonAdapter." 2015-10-08 22:49:11 -04:00
Jesse Wilson
e024ddb431 Merge pull request #97 from DavidMihola/recipe_jsonadapter
Add example for custom JsonAdapter.
2015-10-08 22:45:50 -04:00
DavidMihola
7dc5c25a37 Rewrite @FromJson/@ToJson example. 2015-10-08 20:04:50 +02:00
David Mihola
19c41eb18f Add recipe for @FromJson with non-String input. 2015-10-08 09:48:22 +02:00
David Mihola
2032353ef7 Clean up and simplify by removing Date (use Strings instead). 2015-10-06 22:16:13 +02:00
David Mihola
23135474f4 Add example for custom JsonAdapter. 2015-10-06 17:06:05 +02:00
116 changed files with 3832 additions and 18103 deletions

View file

@ -3,10 +3,10 @@
# Deploy a jar, source jar, and javadoc jar to Sonatype's snapshot repo. # Deploy a jar, source jar, and javadoc jar to Sonatype's snapshot repo.
# #
# Adapted from https://coderwall.com/p/9b_lfq and # Adapted from https://coderwall.com/p/9b_lfq and
# https://benlimmer.com/2013/12/26/automatically-publish-javadoc-to-gh-pages-with-travis-ci/ # http://benlimmer.com/2013/12/26/automatically-publish-javadoc-to-gh-pages-with-travis-ci/
SLUG="square/moshi" SLUG="square/moshi"
JDK="openjdk8" JDK="oraclejdk8"
BRANCH="master" BRANCH="master"
set -e set -e

1
.gitignore vendored
View file

@ -12,7 +12,6 @@ lib
target target
pom.xml.* pom.xml.*
release.properties release.properties
dependency-reduced-pom.xml
.idea .idea
*.iml *.iml

View file

@ -1,7 +1,8 @@
language: java language: java
jdk: jdk:
- openjdk8 - oraclejdk7
- oraclejdk8
after_success: after_success:
- .buildscript/deploy_snapshot.sh - .buildscript/deploy_snapshot.sh
@ -18,6 +19,8 @@ branches:
notifications: notifications:
email: false email: false
sudo: false
cache: cache:
directories: directories:
- $HOME/.m2 - $HOME/.m2

View file

@ -1,315 +1,43 @@
Change Log Change Log
========== ==========
## Version 1.8.0
_2018-11-09_
* New: Support JSON objects that include type information in the JSON. The new
`PolymorphicJsonAdapterFactory` writes a type field when encoding, and reads it when decoding.
* New: Fall back to the reflection-based `KotlinJsonAdapterFactory` if it is enabled and a
generated adapter is not found. This makes it possible to use reflection-based JSON adapters in
development (so you don't have to wait for code to be regenerated on every build) and generated
JSON adapters in production (so you don't need the kotlin-reflect library).
* New: The `peekJson()` method on `JsonReader` let you read ahead on a JSON stream without
consuming it. This builds on Okio's new `Buffer.peek()` API.
* New: The `beginFlatten()` and `endFlatten()` methods on `JsonWriter` suppress unwanted nesting
when composing adapters. Previously it was necessary to flatten objects in memory before writing.
* New: Upgrade to Okio 1.16.0. We don't yet require Kotlin-friendly Okio 2.1 but Moshi works fine
with that release.
```kotlin
implementation("com.squareup.okio:okio:1.16.0")
```
* Fix: Don't return partially-constructed adapters when using a Moshi instance concurrently.
* Fix: Eliminate warnings and errors in generated `.kt` triggered by type variance, primitive
types, and required values.
* Fix: Improve the supplied rules (`moshi.pro`) to better retain symbols used by Moshi. We
recommend R8 when shrinking code.
* Fix: Remove code generation companion objects. This API was neither complete nor necessary.
## Version 1.7.0
_2018-09-24_
* New: `EnumJsonAdapter` makes it easy to specify a fallback value for unknown enum constants.
By default Moshi throws an `JsonDataException` if it reads an unknown enum constant. With this
you can specify a fallback value or null.
```java
new Moshi.Builder()
.add(EnumJsonAdapter.create(IsoCurrency.class)
.withUnknownFallback(IsoCurrency.USD))
.build();
```
Note that this adapter is in the optional `moshi-adapters` module.
```groovy
implementation 'com.squareup.moshi:moshi-adapters:1.7.0'
```
* New: Embed R8/ProGuard rules in the `.jar` file.
* New: Use `@CheckReturnValue` in more places. We hope this will encourage you to use `skipName()`
instead of `nextName()` for better performance!
* New: Forbid automatic encoding of platform classes in `androidx`. As with `java.*`, `android.*`,
and `kotlin.*` Moshi wants you to specify how to encode platform types.
* New: Improve error reporting when creating an adapter fails.
* New: Upgrade to Okio 1.15.0. We don't yet require Kotlin-friendly Okio 2.x but Moshi works fine
with that release.
```groovy
implementation 'com.squareup.okio:okio:1.15.0'
```
* Fix: Return false from `JsonReader.hasNext()` at document's end.
* Fix: Improve code gen to handle several broken cases. Our generated adapters had problems with
nulls, nested parameterized types, private transient properties, generic type aliases, fields
with dollar signs in their names, and named companion objects.
## Version 1.6.0
_2018-05-14_
* **Moshi now supports codegen for Kotlin.** We've added a new annotation processor that generates
a small and fast JSON adapter for your Kotlin types. It can be used on its own or with the
existing `KotlinJsonAdapterFactory` adapter.
* **Moshi now resolves all type parameters.** Previously Moshi wouldn't resolve type parameters on
top-level classes.
* New: Support up to 255 levels of nesting when reading and writing JSON. Previously Moshi would
reject JSON input that contained more than 32 levels of nesting.
* New: Write encoded JSON to a stream with `JsonWriter.value(BufferedSource)`. Use this to emit a
JSON value without decoding it first.
* New: `JsonAdapter.nonNull()` returns a new JSON adapter that forbids explicit nulls in the JSON
body. Use this to detect and fail eagerly on unwanted nulls.
* New: `JsonReader.skipName()` is like `nextName()` but it avoids allocating when a name is
unknown. Use this when `JsonReader.selectName()` returns -1.
* New: Automatic module name of `com.squareup.moshi` for use with the Java Platform Module System.
This moves moshi-adapters into its own `.adapters` package and forwards the existing adapter. It
moves the moshi-kotlin into its own `.kotlin.reflect` package and forwards the existing adapter.
* New: Upgrade to Okio 1.14.0.
```xml
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
<version>1.14.0</version>
</dependency>
com.squareup.okio:okio:1.14.0
```
* Fix: Fail fast if there are trailing non-whitespace characters in the JSON passed to
`JsonAdapter.fromJson(String)`. Previously such data was ignored!
* Fix: Fail fast when Kotlin types are abstract, inner, or object instances.
* Fix: Fail fast if `name()` is called out of sequence.
* Fix: Handle asymmetric `Type.equals()` methods when doing type comparisons. Previously it was
possible that a registered type adapter would not be used because its `Type.equals()` method was
not consistent with a user-provided type.
* Fix: `JsonValueReader.selectString()` now returns -1 for non-strings instead of throwing.
* Fix: Permit reading numbers as strings when the `JsonReader` was created from a JSON value. This
was always supported when reading from a stream but broken when reading from a decoded value.
* Fix: Delegate to user-adapters in the adapter for Object.class. Previously when Moshi encountered
an opaque Object it would only use the built-in adapters. With this change user-installed
adapters for types like `String` will always be honored.
## Version 1.5.0
_2017-05-14_
* **Moshi now uses `@Nullable` to annotate all possibly-null values.** We've added a compile-time
dependency on the JSR 305 annotations. This is a [provided][maven_provided] dependency and does
not need to be included in your build configuration, `.jar` file, or `.apk`. We use
`@ParametersAreNonnullByDefault` and all parameters and return types are never null unless
explicitly annotated `@Nullable`.
* **Warning: Moshi APIs in this update are source-incompatible for Kotlin callers.** Nullability
was previously ambiguous and lenient but now the compiler will enforce strict null checks.
* **Kotlin models are now supported via the `moshi-kotlin` extension.** `KotlinJsonAdapterFactory`
is the best way to use Kotlin with Moshi. It honors default values and is null-safe. Kotlin
users that don't use this factory should write custom adapters for their JSON types. Otherwise
Moshi cannot properly initialize delegated properties of the objects it decodes.
* New: Upgrade to Okio 1.13.0.
```xml
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
<version>1.13.0</version>
</dependency>
com.squareup.okio:okio:1.13.0
```
* New: You may now declare delegates in `@ToJson` and `@FromJson` methods. If one of the arguments
to the method is a `JsonAdapter` of the same type, that will be the next eligible adapter for
that type. This may be useful for composing adapters.
* New: `Types.equals(Type, Type)` makes it easier to compare types in `JsonAdapter.Factory`.
* Fix: Retain the sign on negative zero.
## Version 1.4.0
_2017-02-04_
Moshi 1.4 is a major release that adds _JSON values_ as a core part of the library. We consider any
Java object comprised of maps, lists, strings, numbers, booleans and nulls to be a JSON value. These
are equivalent to parsed JSON objects in JavaScript, [Gson][gson]s `JsonElement`, and
[Jackson][jackson]s `JsonNode`. Unlike Jackson and Gson, Moshi just uses Javas 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&lt;String, Object&gt;</th></tr>
<tr><td>[...]</td><td>Array</td><td>List&lt;Object&gt;</th></tr>
<tr><td>"abc"</td><td>String</td><td>String</th></tr>
<tr><td>123</td><td>Number</td><td>Double, Long, or BigDecimal</th></tr>
<tr><td>true</td><td>Boolean</td><td>Boolean</th></tr>
<tr><td>null</td><td>null</td><td>null</th></tr>
</table>
Moshi's new API `JsonAdapter.toJsonValue()` converts your application classes to JSON values
comprised of the above types. Symmetrically, `JsonAdapter.fromJsonValue()` converts JSON values to
your application classes.
* New: `JsonAdapter.toJsonValue()` and `fromJsonValue()`.
* New: `JsonReader.readJsonValue()` reads a JSON value from a stream.
* New: `Moshi.adapter(Type, Class<? extends Annotation>)` lets you look up the adapter for a
qualified type.
* New: `JsonAdapter.serializeNulls()` and `indent()` return JSON adapters that customize the
format of the encoded JSON.
* New: `JsonReader.selectName()` and `selectString()` optimize decoding JSON with known names and
values.
* New: `Types.nextAnnotations()` reduces the amount of code required to implement a custom
`JsonAdapter.Factory`.
* Fix: Don't fail on large longs that have a fractional component like `9223372036854775806.0`.
## Version 1.3.1
_2016-10-21_
* Fix: Don't incorrectly report invalid input when a slash character is escaped. When we tightened
our invalid escape handling we missed the one character that is valid both escaped `\/` and
unescaped `/`.
## Version 1.3.0
_2016-10-15_
* New: Permit `@ToJson` and `@FromJson` methods to take any number of `JsonAdapter` parameters to
delegate to. This is supported for `@ToJson` methods that take a `JsonWriter` and `@FromJson`
methods that take a `JsonReader`.
* New: Throw `JsonEncodingException` when the incoming data is not valid JSON. Use this to
differentiate data format problems from connectivity problems.
* New: Upgrade to Okio 1.11.0.
```xml
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
<version>1.11.0</version>
</dependency>
```
* New: Omit Kotlin (`kotlin.*`) and Scala (`scala.*`) platform types when encoding objects using
their fields. This should make it easier to avoid unexpected dependencies on platform versions.
* Fix: Explicitly limit reading and writing to 31 levels of nested structure. Previously no
specific limit was enforced, but deeply nested documents would fail with either an
`ArrayIndexOutOfBoundsException` due to a bug in `JsonWriter`'s path management, or a
`StackOverflowError` due to excessive recursion.
* Fix: Require enclosed types to specify their enclosing type with
`Types.newParameterizedTypeWithOwner()`. Previously this API did not exist and looking up
adapters for enclosed parameterized types was not possible.
* Fix: Fail on invalid escapes. Previously any character could be escaped. With this fix only
characters permitted to be escaped may be escaped. Use `JsonReader.setLenient(true)` to read
JSON documents that escape characters that should not be escaped.
## Version 1.2.0
_2016-05-28_
* New: Take advantage of Okio's new `Options` feature when reading field names and enum values.
This has a significant impact on performance. We measured parsing performance improve from 89k
ops/sec to 140k ops/sec on one benchmark on one machine.
* New: Upgrade to Okio 1.8.0.
```xml
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
<version>1.8.0</version>
</dependency>
```
* New: Support types that lack no-argument constructors objects on Android releases prior to
Gingerbread.
* Fix: Add writer value overload for boxed booleans. Autoboxing resolves boxed longs and doubles
to `value(Number)`, but a boxed boolean would otherwise resolve to value(boolean) with an
implicit call to booleanValue() which has the potential to throw NPEs.
* Fix: Be more aggressive about canonicalizing types.
## Version 1.1.0
_2016-01-19_
* New: Support [RFC 7159][rfc_7159], the latest JSON specification. This removes the constraint
that the root value must be an array or an object. It may now take any value: array, object,
string, number, boolean, or null. Previously this was only permitted if the adapter was
configured to be lenient.
* New: Enum constants may be annotated with `@Json` to customize their encoded value.
* New: Create new builder from Moshi instance with `Moshi.newBuilder()`.
* New: `Types.getRawType()` and `Types.collectionElementType()` APIs to assist in defining generic
type adapter factories.
## Version 1.0.0 ## Version 1.0.0
_2015-09-27_ _2015-09-27_
* **API Change**: Replaced `new JsonReader()` with `JsonReader.of()` and `new JsonWriter()` with * **API Change**: Replaced `new JsonReader()` with `JsonReader.of()` and `new JsonWriter()` with
`JsonWriter.of()`. If your code calls either of these constructors it will need to be updated to `JsonWriter.of()`. If your code calls either of these constructors it will need to be updated to
call the static factory method instead. call the static factory method instead.
* **API Change**: Dont throw `IOException` on `JsonAdapter.toJson(T)`. Code that calls this * **API Change**: Dont throw `IOException` on `JsonAdapter.toJson(T)`. Code that calls this method
method may need to be fixed to no longer catch an impossible `IOException`. may need to be fixed to no longer catch an impossible `IOException`.
* Fix: the JSON adapter for `Object` no longer fails when encountering `null` in the stream. * Fix: the JSON adapter for `Object` no longer fails when encountering `null` in the stream.
* New: `@Json` annotation can customize a field's name. This is particularly handy for fields * New: `@Json` annotation can customize a field's name. This is particularly handy for fields whose
whose names are Java keywords, like `default` or `public`. names are Java keywords, like `default` or `public`.
* New: `Rfc3339DateJsonAdapter` converts between a `java.util.Date` and a string formatted with * New: `Rfc3339DateJsonAdapter` converts between a `java.util.Date` and a string formatted with
RFC 3339 (like `2015-09-26T18:23:50.250Z`). This class is in the new `moshi-adapters` RFC 3339 (like `2015-09-26T18:23:50.250Z`). This class is in the new `moshi-adapters` subproject.
subproject. You will need to register this adapter if you want this date formatting behavior. You will need to register this adapter if you want this date formatting behavior. See it in
See it in action in the [dates example][dates_example]. action in the [dates example][dates_example].
* New: `Moshi.adapter()` keeps a cache of all created adapters. For best efficiency, application * New: `Moshi.adapter()` keeps a cache of all created adapters. For best efficiency, application
code should keep a reference to required adapters in a field. code should keep a reference to required adapters in a field.
* New: The `Types` factory class makes it possible to compose types like `List<Card>` or * New: The `Types` factory class makes it possible to compose types like `List<Card>` or
`Map<String, Integer>`. This is useful to look up JSON adapters for parameterized types. `Map<String, Integer>`. This is useful to look up JSON adapters for parameterized types.
* New: `JsonAdapter.failOnUnknown()` returns a new JSON adapter that throws if an unknown value is * New: `JsonAdapter.failOnUnknown()` returns a new JSON adapter that throws if an unknonw value is
encountered on the stream. Use this in development and debug builds to detect typos in field encountered on the stream. Use this in development and debug builds to detect typos in field
names. This feature shouldnt be used in production because it makes migrations very difficult. names. This feature shouldnt be used in production because it makes migrations very difficult.
## Version 0.9.0 ## Version 0.9.0
_2015-06-16_ _2015-06-16_
* Databinding for primitive types, strings, enums, arrays, collections, and maps. * Databinding for primitive types, strings, enums, arrays, collections, and maps.
* Databinding for plain old Java objects. * Databinding for plain old Java objects.
* [JSONPath](http://goessner.net/articles/JsonPath/) support for both `JsonReader` and * [JSONPath](http://goessner.net/articles/JsonPath/) support for both `JsonReader` and
`JsonWriter`. `JsonWriter`.
* Throw `JsonDataException` when theres a data binding problem. * Throw `JsonDataException` when theres a data binding problem.
* Adapter methods: `@ToJson` and `@FromJson`. * Adapter methods: `@ToJson` and `@FromJson`.
* Qualifier annotations: `@JsonQualifier` to permit different type adapters for the same Java * Qualifier annotations: `@JsonQualifier` to permit different type adapters for the same Java type.
type. * Imported code from Gson: `JsonReader`, `JsonWriter`. Also some internal classes:
* Imported code from Gson: `JsonReader`, `JsonWriter`. Also some internal classes: `LinkedHashTreeMap` for hash-collision avoidance and `Types` for typesafe databinding.
`LinkedHashTreeMap` for hash-collision avoidance and `Types` for typesafe databinding.
[dates_example]: https://github.com/square/moshi/blob/master/examples/src/main/java/com/squareup/moshi/recipes/ReadAndWriteRfc3339Dates.java [dates_example]: https://github.com/square/moshi/blob/master/examples/src/main/java/com/squareup/moshi/recipes/ReadAndWriteRfc3339Dates.java
[rfc_7159]: https://tools.ietf.org/html/rfc7159
[gson]: https://github.com/google/gson
[jackson]: http://wiki.fasterxml.com/JacksonHome
[maven_provided]: https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html

443
README.md
View file

@ -120,7 +120,7 @@ Moshi moshi = new Moshi.Builder()
.build(); .build();
``` ```
Voilà: Voila:
```json ```json
{ {
@ -132,146 +132,6 @@ Voilà:
} }
``` ```
#### Another example
Note that the method annotated with `@FromJson` does not need to take a String as an argument.
Rather it can take input of any type and Moshi will first parse the JSON to an object of that type
and then use the `@FromJson` method to produce the desired final value. Conversely, the method
annotated with `@ToJson` does not have to produce a String.
Assume, for example, that we have to parse a JSON in which the date and time of an event are
represented as two separate strings.
```json
{
"title": "Blackjack tournament",
"begin_date": "20151010",
"begin_time": "17:04"
}
```
We would like to combine these two fields into one string to facilitate the date parsing at a
later point. Also, we would like to have all variable names in CamelCase. Therefore, the `Event`
class we want Moshi to produce like this:
```java
class Event {
String title;
String beginDateAndTime;
}
```
Instead of manually parsing the JSON line per line (which we could also do) we can have Moshi do the
transformation automatically. We simply define another class `EventJson` that directly corresponds
to the JSON structure:
```java
class EventJson {
String title;
String begin_date;
String begin_time;
}
```
And another class with the appropriate `@FromJson` and `@ToJson` methods that are telling Moshi how
to convert an `EventJson` to an `Event` and back. Now, whenever we are asking Moshi to parse a JSON
to an `Event` it will first parse it to an `EventJson` as an intermediate step. Conversely, to
serialize an `Event` Moshi will first create an `EventJson` object and then serialize that object as
usual.
```java
class EventJsonAdapter {
@FromJson Event eventFromJson(EventJson eventJson) {
Event event = new Event();
event.title = eventJson.title;
event.beginDateAndTime = eventJson.begin_date + " " + eventJson.begin_time;
return event;
}
@ToJson EventJson eventToJson(Event event) {
EventJson json = new EventJson();
json.title = event.title;
json.begin_date = event.beginDateAndTime.substring(0, 8);
json.begin_time = event.beginDateAndTime.substring(9, 14);
return json;
}
}
```
Again we register the adapter with Moshi.
```java
Moshi moshi = new Moshi.Builder()
.add(new EventJsonAdapter())
.build();
```
We can now use Moshi to parse the JSON directly to an `Event`.
```java
JsonAdapter<Event> jsonAdapter = moshi.adapter(Event.class);
Event event = jsonAdapter.fromJson(json);
```
### Adapter convenience methods
Moshi provides a number of convenience methods for `JsonAdapter` objects:
- `nullSafe()`
- `nonNull()`
- `lenient()`
- `failOnUnknown()`
- `indent()`
- `serializeNulls()`
These factory methods wrap an existing `JsonAdapter` into additional functionality.
For example, if you have an adapter that doesn't support nullable values, you can use `nullSafe()` to make it null safe:
```java
String dateJson = "\"2018-11-26T11:04:19.342668Z\"";
String nullDateJson = "null";
// RFC 3339 date adapter, doesn't support null by default
// See also: https://github.com/square/moshi/tree/master/adapters
JsonAdapter<Date> adapter = new Rfc3339DateJsonAdapter();
Date date = adapter.fromJson(dateJson);
System.out.println(date); // Mon Nov 26 12:04:19 CET 2018
Date nullDate = adapter.fromJson(nullDateJson);
// Exception, com.squareup.moshi.JsonDataException: Expected a string but was NULL at path $
Date nullDate = adapter.nullSafe().fromJson(nullDateJson);
System.out.println(nullDate); // null
```
In contrast to `nullSafe()` there is `nonNull()` to make an adapter refuse null values. Refer to the Moshi JavaDoc for details on the various methods.
### Parse JSON Arrays
Say we have a JSON string of this structure:
```json
[
{
"rank": "4",
"suit": "CLUBS"
},
{
"rank": "A",
"suit": "HEARTS"
}
]
```
We can now use Moshi to parse the JSON string into a `List<Card>`.
```java
String cardsJsonResponse = ...;
Type type = Types.newParameterizedType(List.class, Card.class);
JsonAdapter<List<Card>> adapter = moshi.adapter(type);
List<Card> cards = adapter.fromJson(cardsJsonResponse);
```
### Fails Gracefully ### Fails Gracefully
Automatic databinding almost feels like magic. But unlike the black magic that typically accompanies Automatic databinding almost feels like magic. But unlike the black magic that typically accompanies
@ -316,310 +176,28 @@ But the two libraries have a few important differences:
encoded in HTML without additional escaping. Moshi encodes it naturally (as `=`) and assumes that encoded in HTML without additional escaping. Moshi encodes it naturally (as `=`) and assumes that
the HTML encoder if there is one will do its job. the HTML encoder if there is one will do its job.
### Custom field names with @Json
Moshi works best when your JSON objects and Java objects have the same structure. But when they
don't, Moshi has annotations to customize data binding.
Use `@Json` to specify how Java fields map to JSON names. This is necessary when the JSON name
contains spaces or other characters that arent 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.
Heres 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
shouldnt need this `@JsonQualifier`, but its very handy for those that do.
### Omit fields with `transient`
Some models declare fields that shouldnt 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 Javas `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 fields 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 doesnt have a no-arguments constructor, Moshi cant assign the fields default value,
**even if its specified in the field declaration**. Instead, the fields 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 Kotlins 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 Kotlins 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()
```
Moshis 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
Moshis 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 Kotlins default visibility).
Kotlin codegen has no additional runtime dependency. Youll need to [enable kapt][kapt] and then
add the following to your build to enable the annotation processor:
```xml
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-kotlin-codegen</artifactId>
<version>1.8.0</version>
<scope>provided</scope>
</dependency>
```
```kotlin
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.8.0")
```
You must also have the `kotlin-stdlib` dependency on the classpath during compilation in order for
the compiled code to have the required metadata annotations that Moshi's processor looks for.
#### Limitations
If your Kotlin class has a superclass, it must also be a Kotlin class. Neither reflection or codegen
support Kotlin types with Java supertypes or Java types with Kotlin supertypes. If you need to
convert such classes to JSON you must create a custom type adapter.
The JSON encoding of Kotlin types is the same whether using reflection or codegen. Prefer codegen
for better performance and to avoid the `kotlin-reflect` dependency; prefer reflection to convert
both private and protected properties. If you have configured both, generated adapters will be used
on types that are annotated `@JsonClass(generateAdapter = true)`.
Download Download
-------- --------
Download [the latest JAR][dl] or depend via Maven: **Moshi is under development.** The API is not final. Download [the latest .jar][dl] or depend via
Maven:
```xml ```xml
<dependency> <dependency>
<groupId>com.squareup.moshi</groupId> <groupId>com.squareup.moshi</groupId>
<artifactId>moshi</artifactId> <artifactId>moshi</artifactId>
<version>1.8.0</version> <version>1.0.0</version>
</dependency> </dependency>
``` ```
or Gradle: or Gradle:
```kotlin ```groovy
implementation("com.squareup.moshi:moshi:1.8.0") compile 'com.squareup.moshi:moshi:1.0.0'
``` ```
Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap].
R8 / ProGuard
--------
If you are using R8 or ProGuard add the options from [this file](https://github.com/square/moshi/blob/master/moshi/src/main/resources/META-INF/proguard/moshi.pro). If using Android, this requires Android Gradle Plugin 3.2.0+.
The `moshi-kotlin` artifact additionally requires the options from [this file](https://github.com/square/moshi/blob/master/kotlin/reflect/src/main/resources/META-INF/proguard/moshi-kotlin.pro)
You might also need rules for Okio which is a dependency of this library.
License License
-------- --------
@ -638,10 +216,9 @@ License
limitations under the License. limitations under the License.
[dl]: https://search.maven.org/classic/remote_content?g=com.squareup.moshi&a=moshi&v=LATEST [dl]: https://search.maven.org/remote_content?g=com.squareup.moshi&a=moshi&v=LATEST
[snap]: https://oss.sonatype.org/content/repositories/snapshots/com/squareup/moshi/ [snap]: https://oss.sonatype.org/content/repositories/snapshots/com/squareup/moshi/
[okio]: https://github.com/square/okio/ [okio]: https://github.com/square/okio/
[okhttp]: https://github.com/square/okhttp/ [okhttp]: https://github.com/square/okhttp
[gson]: https://github.com/google/gson/ [gson]: https://github.com/google/gson
[javadoc]: https://square.github.io/moshi/1.x/moshi/ [javadoc]: https://square.github.io/moshi/
[kapt]: https://kotlinlang.org/docs/reference/kapt.html

View file

@ -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/

View file

@ -6,7 +6,7 @@
<parent> <parent>
<groupId>com.squareup.moshi</groupId> <groupId>com.squareup.moshi</groupId>
<artifactId>moshi-parent</artifactId> <artifactId>moshi-parent</artifactId>
<version>1.9.0-SNAPSHOT</version> <version>1.1.0-SNAPSHOT</version>
</parent> </parent>
<artifactId>moshi-adapters</artifactId> <artifactId>moshi-adapters</artifactId>
@ -17,11 +17,6 @@
<artifactId>moshi</artifactId> <artifactId>moshi</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>
<artifactId>junit</artifactId> <artifactId>junit</artifactId>
@ -33,20 +28,4 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
</dependencies> </dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Automatic-Module-Name>com.squareup.moshi.adapters</Automatic-Module-Name>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project> </project>

View file

@ -13,9 +13,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.squareup.moshi.adapters; package com.squareup.moshi;
import com.squareup.moshi.JsonDataException;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
@ -24,7 +23,7 @@ import java.util.TimeZone;
/** /**
* Jacksons date formatter, pruned to Moshi's needs. Forked from this file: * Jacksons date formatter, pruned to Moshi's needs. Forked from this file:
* https://github.com/FasterXML/jackson-databind/blob/67ebf7305f492285a8f9f4de31545f5f16fc7c3a/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java * https://github.com/FasterXML/jackson-databind/blob/master/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java
* *
* Utilities methods for manipulating dates in iso8601 format. This is much much faster and GC * Utilities methods for manipulating dates in iso8601 format. This is much much faster and GC
* friendly than using SimpleDateFormat so highly suitable if you (un)serialize lots of date * friendly than using SimpleDateFormat so highly suitable if you (un)serialize lots of date
@ -191,7 +190,7 @@ final class Iso8601Utils {
// If we get a ParseException it'll already have the right message/offset. // If we get a ParseException it'll already have the right message/offset.
// Other exception types can convert here. // Other exception types can convert here.
} catch (IndexOutOfBoundsException | IllegalArgumentException e) { } catch (IndexOutOfBoundsException | IllegalArgumentException e) {
throw new JsonDataException("Not an RFC 3339 date: " + date, e); throw new JsonDataException("Not an RFC 3339 date: " + date);
} }
} }

View file

@ -19,18 +19,17 @@ import java.io.IOException;
import java.util.Date; import java.util.Date;
/** /**
* @deprecated this class moved to avoid a package name conflict in the Java Platform Module System. * Formats dates using <a href="https://www.ietf.org/rfc/rfc3339.txt">RFC 3339</a>, which is
* The new class is {@code com.squareup.moshi.adapters.Rfc3339DateJsonAdapter}. * formatted like {@code 2015-09-26T18:23:50.250Z}.
*/ */
public final class Rfc3339DateJsonAdapter extends JsonAdapter<Date> { public final class Rfc3339DateJsonAdapter extends JsonAdapter<Date> {
com.squareup.moshi.adapters.Rfc3339DateJsonAdapter delegate @Override public synchronized Date fromJson(JsonReader reader) throws IOException {
= new com.squareup.moshi.adapters.Rfc3339DateJsonAdapter(); String string = reader.nextString();
return Iso8601Utils.parse(string);
@Override public Date fromJson(JsonReader reader) throws IOException {
return delegate.fromJson(reader);
} }
@Override public void toJson(JsonWriter writer, Date value) throws IOException { @Override public synchronized void toJson(JsonWriter writer, Date value) throws IOException {
delegate.toJson(writer, value); String string = Iso8601Utils.format(value);
writer.value(string);
} }
} }

View file

@ -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() + ")";
}
}

View file

@ -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 objects 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 + ")";
}
}
}

View file

@ -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);
}
}

View file

@ -13,9 +13,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.squareup.moshi.adapters; package com.squareup.moshi;
import com.squareup.moshi.JsonAdapter;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;

View file

@ -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
}
}

View file

@ -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;
}
}
}

View file

@ -1,10 +1,9 @@
<?xml version="1.0"?> <?xml version="1.0"?>
<!DOCTYPE module PUBLIC <!DOCTYPE module PUBLIC
"-//Puppy Crawl//DTD Check Configuration 1.3//EN" "-//Puppy Crawl//DTD Check Configuration 1.2//EN"
"http://www.puppycrawl.com/dtds/configuration_1_3.dtd"> "http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
<module name="Checker"> <module name="Checker">
<module name="SuppressWarningsFilter"/>
<module name="NewlineAtEndOfFile"/> <module name="NewlineAtEndOfFile"/>
<module name="FileLength"/> <module name="FileLength"/>
<module name="FileTabCharacter"/> <module name="FileTabCharacter"/>
@ -45,7 +44,7 @@
<module name="LocalVariableName"/> <module name="LocalVariableName"/>
<module name="MemberName"/> <module name="MemberName"/>
<module name="MethodName"/> <module name="MethodName"/>
<!--<module name="PackageName"/>--> <module name="PackageName"/>
<module name="ParameterName"/> <module name="ParameterName"/>
<module name="StaticVariableName"/> <module name="StaticVariableName"/>
<module name="TypeName"/> <module name="TypeName"/>
@ -57,9 +56,7 @@
<module name="IllegalImport"/> <module name="IllegalImport"/>
<!-- defaults to sun.* packages --> <!-- defaults to sun.* packages -->
<module name="RedundantImport"/> <module name="RedundantImport"/>
<module name="UnusedImports"> <module name="UnusedImports"/>
<property name="processJavadoc" value="true"/>
</module>
<!-- Checks for Size Violations. --> <!-- Checks for Size Violations. -->
@ -67,9 +64,7 @@
<module name="LineLength"> <module name="LineLength">
<property name="max" value="100"/> <property name="max" value="100"/>
</module> </module>
<module name="MethodLength"> <module name="MethodLength"/>
<property name="max" value="200"/>
</module>
<!-- Checks for whitespace --> <!-- Checks for whitespace -->
@ -83,15 +78,7 @@
<module name="ParenPad"/> <module name="ParenPad"/>
<module name="TypecastParenPad"/> <module name="TypecastParenPad"/>
<module name="WhitespaceAfter"/> <module name="WhitespaceAfter"/>
<module name="WhitespaceAround"> <module name="WhitespaceAround"/>
<property name="tokens"
value="ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR, BXOR_ASSIGN,
COLON, DIV, DIV_ASSIGN, DO_WHILE, EQUAL, GE, GT, LAND, LCURLY, LE, LITERAL_CATCH,
LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF, LITERAL_RETURN,
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS,
MINUS_ASSIGN, MOD, MOD_ASSIGN, NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, SL, SLIST,
SL_ASSIGN, SR, SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT, TYPE_EXTENSION_AND"/>
</module>
<!-- Modifier Checks --> <!-- Modifier Checks -->
@ -121,7 +108,7 @@
<!--module name="InnerAssignment"/--> <!--module name="InnerAssignment"/-->
<!--module name="MagicNumber"/--> <!--module name="MagicNumber"/-->
<!--module name="MissingSwitchDefault"/--> <!--module name="MissingSwitchDefault"/-->
<!--<module name="RedundantThrows"/>--> <module name="RedundantThrows"/>
<module name="SimplifyBooleanExpression"/> <module name="SimplifyBooleanExpression"/>
<module name="SimplifyBooleanReturn"/> <module name="SimplifyBooleanReturn"/>
@ -140,8 +127,5 @@
<!--module name="FinalParameters"/--> <!--module name="FinalParameters"/-->
<!--module name="TodoComment"/--> <!--module name="TodoComment"/-->
<module name="UpperEll"/> <module name="UpperEll"/>
<!-- Make the @SuppressWarnings annotations available to Checkstyle -->
<module name="SuppressWarningsHolder"/>
</module> </module>
</module> </module>

View file

@ -6,17 +6,12 @@
<parent> <parent>
<groupId>com.squareup.moshi</groupId> <groupId>com.squareup.moshi</groupId>
<artifactId>moshi-parent</artifactId> <artifactId>moshi-parent</artifactId>
<version>1.9.0-SNAPSHOT</version> <version>1.1.0-SNAPSHOT</version>
</parent> </parent>
<artifactId>moshi-examples</artifactId> <artifactId>moshi-examples</artifactId>
<dependencies> <dependencies>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>com.squareup.moshi</groupId> <groupId>com.squareup.moshi</groupId>
<artifactId>moshi</artifactId> <artifactId>moshi</artifactId>

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
};
}
}

View file

@ -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() {
}
}

View file

@ -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;
}
}
}

View file

@ -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();
}
}

View file

@ -17,7 +17,7 @@ package com.squareup.moshi.recipes;
import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi; import com.squareup.moshi.Moshi;
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter; import com.squareup.moshi.Rfc3339DateJsonAdapter;
import com.squareup.moshi.recipes.models.Tournament; import com.squareup.moshi.recipes.models.Tournament;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;

View file

@ -21,7 +21,7 @@ import com.squareup.moshi.recipes.models.BlackjackHand;
public final class ReadJson { public final class ReadJson {
public void run() throws Exception { public void run() throws Exception {
String json = "" String json = ""
+ "{\n" + "{\n"
+ " \"hidden_card\": {\n" + " \"hidden_card\": {\n"
+ " \"rank\": \"6\",\n" + " \"rank\": \"6\",\n"

View file

@ -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();
}
}

View file

@ -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));
}
}
}

View file

@ -17,14 +17,13 @@ package com.squareup.moshi.recipes.models;
import java.util.List; import java.util.List;
@SuppressWarnings("checkstyle:membername")
public final class BlackjackHand { public final class BlackjackHand {
public final Card hidden_card; public final Card hidden_card;
public final List<Card> visible_cards; public final List<Card> visible_cards;
public BlackjackHand(Card hiddenCard, List<Card> visibleCards) { public BlackjackHand(Card hidden_card, List<Card> visible_cards) {
this.hidden_card = hiddenCard; this.hidden_card = hidden_card;
this.visible_cards = visibleCards; this.visible_cards = visible_cards;
} }
@Override public String toString() { @Override public String toString() {

View file

@ -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;
}
}

View file

@ -1,3 +0,0 @@
/** Moshi code samples. */
@javax.annotation.ParametersAreNonnullByDefault
package com.squareup.moshi.recipes;

View file

@ -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>

View file

@ -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>

View file

@ -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()
}
}

View file

@ -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)
}
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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()
}
}

View file

@ -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)
}
}
}

View file

@ -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
)

View file

@ -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
}

View file

@ -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
}
}
}
}
}

View file

@ -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
}
}
}

View file

@ -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")
}
}
}

View file

@ -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")
}
}

View file

@ -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)
}

View file

@ -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;
}

View file

@ -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")
}
}

View file

@ -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()
}
}

View file

@ -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
)

View file

@ -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()
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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()

View file

@ -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()
}
}

View file

@ -1,5 +0,0 @@
-keep class kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoaderImpl
-keepclassmembers class kotlin.Metadata {
public <methods>;
}

View file

@ -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()"
)
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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()
}
}

View file

@ -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)

View file

@ -6,7 +6,7 @@
<parent> <parent>
<groupId>com.squareup.moshi</groupId> <groupId>com.squareup.moshi</groupId>
<artifactId>moshi-parent</artifactId> <artifactId>moshi-parent</artifactId>
<version>1.9.0-SNAPSHOT</version> <version>1.1.0-SNAPSHOT</version>
</parent> </parent>
<artifactId>moshi</artifactId> <artifactId>moshi</artifactId>
@ -17,11 +17,6 @@
<groupId>com.squareup.okio</groupId> <groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId> <artifactId>okio</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>junit</groupId> <groupId>junit</groupId>
<artifactId>junit</artifactId> <artifactId>junit</artifactId>
@ -33,31 +28,4 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
</dependencies> </dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Automatic-Module-Name>com.squareup.moshi</Automatic-Module-Name>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.10.4</version>
<configuration>
<excludePackageNames>com.squareup.moshi.internal:com.squareup.moshi.internal.*</excludePackageNames>
<links>
<link>https://square.github.io/okio/</link>
</links>
</configuration>
</plugin>
</plugins>
</build>
</project> </project>

View file

@ -15,32 +15,25 @@
*/ */
package com.squareup.moshi; package com.squareup.moshi;
import com.squareup.moshi.internal.Util;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import javax.annotation.Nullable;
import static com.squareup.moshi.internal.Util.canonicalize;
import static com.squareup.moshi.internal.Util.jsonAnnotations;
import static com.squareup.moshi.internal.Util.typeAnnotatedWithAnnotations;
final class AdapterMethodsFactory implements JsonAdapter.Factory { final class AdapterMethodsFactory implements JsonAdapter.Factory {
private final List<AdapterMethod> toAdapters; private final List<AdapterMethod> toAdapters;
private final List<AdapterMethod> fromAdapters; private final List<AdapterMethod> fromAdapters;
AdapterMethodsFactory(List<AdapterMethod> toAdapters, List<AdapterMethod> fromAdapters) { public AdapterMethodsFactory(List<AdapterMethod> toAdapters, List<AdapterMethod> fromAdapters) {
this.toAdapters = toAdapters; this.toAdapters = toAdapters;
this.fromAdapters = fromAdapters; this.fromAdapters = fromAdapters;
} }
@Override public @Nullable JsonAdapter<?> create( @Override public JsonAdapter<?> create(
final Type type, final Set<? extends Annotation> annotations, final Moshi moshi) { final Type type, final Set<? extends Annotation> annotations, final Moshi moshi) {
final AdapterMethod toAdapter = get(toAdapters, type, annotations); final AdapterMethod toAdapter = get(toAdapters, type, annotations);
final AdapterMethod fromAdapter = get(fromAdapters, type, annotations); final AdapterMethod fromAdapter = get(fromAdapters, type, annotations);
@ -53,17 +46,14 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
String missingAnnotation = toAdapter == null ? "@ToJson" : "@FromJson"; String missingAnnotation = toAdapter == null ? "@ToJson" : "@FromJson";
throw new IllegalArgumentException("No " + missingAnnotation + " adapter for " throw new IllegalArgumentException("No " + missingAnnotation + " adapter for "
+ typeAnnotatedWithAnnotations(type, annotations), e); + type + " annotated " + annotations);
} }
} else { } else {
delegate = null; delegate = null;
} }
if (toAdapter != null) toAdapter.bind(moshi, this);
if (fromAdapter != null) fromAdapter.bind(moshi, this);
return new JsonAdapter<Object>() { return new JsonAdapter<Object>() {
@Override public void toJson(JsonWriter writer, @Nullable Object value) throws IOException { @Override public void toJson(JsonWriter writer, Object value) throws IOException {
if (toAdapter == null) { if (toAdapter == null) {
delegate.toJson(writer, value); delegate.toJson(writer, value);
} else if (!toAdapter.nullable && value == null) { } else if (!toAdapter.nullable && value == null) {
@ -71,15 +61,16 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
} else { } else {
try { try {
toAdapter.toJson(moshi, writer, value); toAdapter.toJson(moshi, writer, value);
} catch (IllegalAccessException e) {
throw new AssertionError();
} catch (InvocationTargetException e) { } catch (InvocationTargetException e) {
Throwable cause = e.getCause(); if (e.getCause() instanceof IOException) throw (IOException) e.getCause();
if (cause instanceof IOException) throw (IOException) cause; throw new JsonDataException(e.getCause() + " at " + writer.getPath());
throw new JsonDataException(cause + " at " + writer.getPath(), cause);
} }
} }
} }
@Override public @Nullable Object fromJson(JsonReader reader) throws IOException { @Override public Object fromJson(JsonReader reader) throws IOException {
if (fromAdapter == null) { if (fromAdapter == null) {
return delegate.fromJson(reader); return delegate.fromJson(reader);
} else if (!fromAdapter.nullable && reader.peek() == JsonReader.Token.NULL) { } else if (!fromAdapter.nullable && reader.peek() == JsonReader.Token.NULL) {
@ -88,10 +79,11 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
} else { } else {
try { try {
return fromAdapter.fromJson(moshi, reader); return fromAdapter.fromJson(moshi, reader);
} catch (IllegalAccessException e) {
throw new AssertionError();
} catch (InvocationTargetException e) { } catch (InvocationTargetException e) {
Throwable cause = e.getCause(); if (e.getCause() instanceof IOException) throw (IOException) e.getCause();
if (cause instanceof IOException) throw (IOException) cause; throw new JsonDataException(e.getCause() + " at " + reader.getPath());
throw new JsonDataException(cause + " at " + reader.getPath(), cause);
} }
} }
} }
@ -146,46 +138,34 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
*/ */
static AdapterMethod toAdapter(Object adapter, Method method) { static AdapterMethod toAdapter(Object adapter, Method method) {
method.setAccessible(true); method.setAccessible(true);
Type[] parameterTypes = method.getGenericParameterTypes();
final Type returnType = method.getGenericReturnType(); final Type returnType = method.getGenericReturnType();
final Type[] parameterTypes = method.getGenericParameterTypes();
final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
if (parameterTypes.length >= 2 if (parameterTypes.length == 2
&& parameterTypes[0] == JsonWriter.class && parameterTypes[0] == JsonWriter.class
&& returnType == void.class && returnType == void.class) {
&& parametersAreJsonAdapters(2, parameterTypes)) { // public void pointToJson(JsonWriter jsonWriter, Point point) throws Exception {
// void pointToJson(JsonWriter jsonWriter, Point point) { Set<? extends Annotation> parameterAnnotations
// void pointToJson(JsonWriter jsonWriter, Point point, JsonAdapter<?> adapter, ...) { = Util.jsonAnnotations(method.getParameterAnnotations()[1]);
Set<? extends Annotation> qualifierAnnotations = jsonAnnotations(parameterAnnotations[1]); return new AdapterMethod(parameterTypes[1], parameterAnnotations, adapter, method, false) {
return new AdapterMethod(parameterTypes[1], qualifierAnnotations, adapter, method, @Override public void toJson(Moshi moshi, JsonWriter writer, Object value)
parameterTypes.length, 2, true) { throws IOException, InvocationTargetException, IllegalAccessException {
@Override public void toJson(Moshi moshi, JsonWriter writer, @Nullable Object value) method.invoke(adapter, writer, value);
throws IOException, InvocationTargetException {
invoke(writer, value);
} }
}; };
} else if (parameterTypes.length == 1 && returnType != void.class) { } else if (parameterTypes.length == 1 && returnType != void.class) {
// List<Integer> pointToJson(Point point) { // public List<Integer> pointToJson(Point point) throws Exception {
final Set<? extends Annotation> returnTypeAnnotations = jsonAnnotations(method); final Set<? extends Annotation> returnTypeAnnotations = Util.jsonAnnotations(method);
final Set<? extends Annotation> qualifierAnnotations = Annotation[][] parameterAnnotations = method.getParameterAnnotations();
jsonAnnotations(parameterAnnotations[0]); Set<? extends Annotation> qualifierAnnotations =
Util.jsonAnnotations(parameterAnnotations[0]);
boolean nullable = Util.hasNullable(parameterAnnotations[0]); boolean nullable = Util.hasNullable(parameterAnnotations[0]);
return new AdapterMethod(parameterTypes[0], qualifierAnnotations, adapter, method, return new AdapterMethod(parameterTypes[0], qualifierAnnotations, adapter, method, nullable) {
parameterTypes.length, 1, nullable) { @Override public void toJson(Moshi moshi, JsonWriter writer, Object value)
private JsonAdapter<Object> delegate; throws IOException, InvocationTargetException, IllegalAccessException {
JsonAdapter<Object> delegate = moshi.adapter(returnType, returnTypeAnnotations);
@Override public void bind(Moshi moshi, JsonAdapter.Factory factory) { Object intermediate = method.invoke(adapter, value);
super.bind(moshi, factory);
delegate = Types.equals(parameterTypes[0], returnType)
&& qualifierAnnotations.equals(returnTypeAnnotations)
? moshi.nextAdapter(factory, returnType, returnTypeAnnotations)
: moshi.adapter(returnType, returnTypeAnnotations);
}
@Override public void toJson(Moshi moshi, JsonWriter writer, @Nullable Object value)
throws IOException, InvocationTargetException {
Object intermediate = invoke(value);
delegate.toJson(writer, intermediate); delegate.toJson(writer, intermediate);
} }
}; };
@ -194,163 +174,91 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
throw new IllegalArgumentException("Unexpected signature for " + method + ".\n" throw new IllegalArgumentException("Unexpected signature for " + method + ".\n"
+ "@ToJson method signatures may have one of the following structures:\n" + "@ToJson method signatures may have one of the following structures:\n"
+ " <any access modifier> void toJson(JsonWriter writer, T value) throws <any>;\n" + " <any access modifier> void toJson(JsonWriter writer, T value) throws <any>;\n"
+ " <any access modifier> void toJson(JsonWriter writer, T value,"
+ " JsonAdapter<any> delegate, <any more delegates>) throws <any>;\n"
+ " <any access modifier> R toJson(T value) throws <any>;\n"); + " <any access modifier> R toJson(T value) throws <any>;\n");
} }
} }
/** Returns true if {@code parameterTypes[offset..]} contains only JsonAdapters. */
private static boolean parametersAreJsonAdapters(int offset, Type[] parameterTypes) {
for (int i = offset, length = parameterTypes.length; i < length; i++) {
if (!(parameterTypes[i] instanceof ParameterizedType)) return false;
if (((ParameterizedType) parameterTypes[i]).getRawType() != JsonAdapter.class) return false;
}
return true;
}
/** /**
* Returns an object that calls a {@code method} method on {@code adapter} in service of * Returns an object that calls a {@code method} method on {@code adapter} in service of
* converting an object from JSON. * converting an object from JSON.
*/ */
static AdapterMethod fromAdapter(Object adapter, Method method) { static AdapterMethod fromAdapter(Object adapter, Method method) {
method.setAccessible(true); method.setAccessible(true);
final Type returnType = method.getGenericReturnType();
final Set<? extends Annotation> returnTypeAnnotations = jsonAnnotations(method);
final Type[] parameterTypes = method.getGenericParameterTypes(); final Type[] parameterTypes = method.getGenericParameterTypes();
Annotation[][] parameterAnnotations = method.getParameterAnnotations(); final Type returnType = method.getGenericReturnType();
if (parameterTypes.length >= 1 if (parameterTypes.length == 1
&& parameterTypes[0] == JsonReader.class && parameterTypes[0] == JsonReader.class
&& returnType != void.class && returnType != void.class) {
&& parametersAreJsonAdapters(1, parameterTypes)) { // public Point pointFromJson(JsonReader jsonReader) throws Exception {
// Point pointFromJson(JsonReader jsonReader) { Set<? extends Annotation> returnTypeAnnotations = Util.jsonAnnotations(method);
// Point pointFromJson(JsonReader jsonReader, JsonAdapter<?> adapter, ...) { return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method, false) {
return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method,
parameterTypes.length, 1, true) {
@Override public Object fromJson(Moshi moshi, JsonReader reader) @Override public Object fromJson(Moshi moshi, JsonReader reader)
throws IOException, InvocationTargetException { throws IOException, IllegalAccessException, InvocationTargetException {
return invoke(reader); return method.invoke(adapter, reader);
} }
}; };
} else if (parameterTypes.length == 1 && returnType != void.class) { } else if (parameterTypes.length == 1 && returnType != void.class) {
// Point pointFromJson(List<Integer> o) { // public Point pointFromJson(List<Integer> o) throws Exception {
Set<? extends Annotation> returnTypeAnnotations = Util.jsonAnnotations(method);
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
final Set<? extends Annotation> qualifierAnnotations final Set<? extends Annotation> qualifierAnnotations
= jsonAnnotations(parameterAnnotations[0]); = Util.jsonAnnotations(parameterAnnotations[0]);
boolean nullable = Util.hasNullable(parameterAnnotations[0]); boolean nullable = Util.hasNullable(parameterAnnotations[0]);
return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method, return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method, nullable) {
parameterTypes.length, 1, nullable) {
JsonAdapter<Object> delegate;
@Override public void bind(Moshi moshi, JsonAdapter.Factory factory) {
super.bind(moshi, factory);
delegate = Types.equals(parameterTypes[0], returnType)
&& qualifierAnnotations.equals(returnTypeAnnotations)
? moshi.nextAdapter(factory, parameterTypes[0], qualifierAnnotations)
: moshi.adapter(parameterTypes[0], qualifierAnnotations);
}
@Override public Object fromJson(Moshi moshi, JsonReader reader) @Override public Object fromJson(Moshi moshi, JsonReader reader)
throws IOException, InvocationTargetException { throws IOException, IllegalAccessException, InvocationTargetException {
JsonAdapter<Object> delegate = moshi.adapter(parameterTypes[0], qualifierAnnotations);
Object intermediate = delegate.fromJson(reader); Object intermediate = delegate.fromJson(reader);
return invoke(intermediate); return method.invoke(adapter, intermediate);
} }
}; };
} else { } else {
throw new IllegalArgumentException("Unexpected signature for " + method + ".\n" throw new IllegalArgumentException("Unexpected signature for " + method + ".\n"
+ "@FromJson method signatures may have one of the following structures:\n" + "@ToJson method signatures may have one of the following structures:\n"
+ " <any access modifier> R fromJson(JsonReader jsonReader) throws <any>;\n" + " <any access modifier> void toJson(JsonWriter writer, T value) throws <any>;\n"
+ " <any access modifier> R fromJson(JsonReader jsonReader," + " <any access modifier> R toJson(T value) throws <any>;\n");
+ " JsonAdapter<any> delegate, <any more delegates>) throws <any>;\n"
+ " <any access modifier> R fromJson(T value) throws <any>;\n");
} }
} }
/** Returns the matching adapter method from the list. */ /** Returns the matching adapter method from the list. */
private static @Nullable AdapterMethod get( private static AdapterMethod get(
List<AdapterMethod> adapterMethods, Type type, Set<? extends Annotation> annotations) { List<AdapterMethod> adapterMethods, Type type, Set<? extends Annotation> annotations) {
for (int i = 0, size = adapterMethods.size(); i < size; i++) { for (int i = 0, size = adapterMethods.size(); i < size; i++) {
AdapterMethod adapterMethod = adapterMethods.get(i); AdapterMethod adapterMethod = adapterMethods.get(i);
if (Types.equals(adapterMethod.type, type) && adapterMethod.annotations.equals(annotations)) { if (adapterMethod.type.equals(type) && adapterMethod.annotations.equals(annotations)) {
return adapterMethod; return adapterMethod;
} }
} }
return null; return null;
} }
abstract static class AdapterMethod { static abstract class AdapterMethod {
final Type type; final Type type;
final Set<? extends Annotation> annotations; final Set<? extends Annotation> annotations;
final Object adapter; final Object adapter;
final Method method; final Method method;
final int adaptersOffset;
final JsonAdapter<?>[] jsonAdapters;
final boolean nullable; final boolean nullable;
AdapterMethod(Type type, Set<? extends Annotation> annotations, Object adapter, public AdapterMethod(Type type,
Method method, int parameterCount, int adaptersOffset, boolean nullable) { Set<? extends Annotation> annotations, Object adapter, Method method, boolean nullable) {
this.type = canonicalize(type); this.type = type;
this.annotations = annotations; this.annotations = annotations;
this.adapter = adapter; this.adapter = adapter;
this.method = method; this.method = method;
this.adaptersOffset = adaptersOffset;
this.jsonAdapters = new JsonAdapter[parameterCount - adaptersOffset];
this.nullable = nullable; this.nullable = nullable;
} }
public void bind(Moshi moshi, JsonAdapter.Factory factory) { public void toJson(Moshi moshi, JsonWriter writer, Object value)
if (jsonAdapters.length > 0) { throws IOException, IllegalAccessException, InvocationTargetException {
Type[] parameterTypes = method.getGenericParameterTypes();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
for (int i = adaptersOffset, size = parameterTypes.length; i < size; i++) {
Type type = ((ParameterizedType) parameterTypes[i]).getActualTypeArguments()[0];
Set<? extends Annotation> jsonAnnotations = jsonAnnotations(parameterAnnotations[i]);
jsonAdapters[i - adaptersOffset] =
Types.equals(this.type, type) && annotations.equals(jsonAnnotations)
? moshi.nextAdapter(factory, type, jsonAnnotations)
: moshi.adapter(type, jsonAnnotations);
}
}
}
public void toJson(Moshi moshi, JsonWriter writer, @Nullable Object value)
throws IOException, InvocationTargetException {
throw new AssertionError(); throw new AssertionError();
} }
public @Nullable Object fromJson(Moshi moshi, JsonReader reader) public Object fromJson(Moshi moshi, JsonReader reader)
throws IOException, InvocationTargetException { throws IOException, IllegalAccessException, InvocationTargetException {
throw new AssertionError(); throw new AssertionError();
} }
/** Invoke the method with one fixed argument, plus any number of JSON adapter arguments. */
protected @Nullable Object invoke(@Nullable Object a1) throws InvocationTargetException {
Object[] args = new Object[1 + jsonAdapters.length];
args[0] = a1;
System.arraycopy(jsonAdapters, 0, args, 1, jsonAdapters.length);
try {
return method.invoke(adapter, args);
} catch (IllegalAccessException e) {
throw new AssertionError();
}
}
/** Invoke the method with two fixed arguments, plus any number of JSON adapter arguments. */
protected Object invoke(@Nullable Object a1, @Nullable Object a2)
throws InvocationTargetException {
Object[] args = new Object[2 + jsonAdapters.length];
args[0] = a1;
args[1] = a2;
System.arraycopy(jsonAdapters, 0, args, 2, jsonAdapters.length);
try {
return method.invoke(adapter, args);
} catch (IllegalAccessException e) {
throw new AssertionError();
}
}
} }
} }

View file

@ -22,7 +22,6 @@ import java.lang.reflect.Type;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import javax.annotation.Nullable;
/** /**
* Converts arrays to JSON arrays containing their converted contents. This * Converts arrays to JSON arrays containing their converted contents. This
@ -30,7 +29,7 @@ import javax.annotation.Nullable;
*/ */
final class ArrayJsonAdapter extends JsonAdapter<Object> { final class ArrayJsonAdapter extends JsonAdapter<Object> {
public static final Factory FACTORY = new Factory() { public static final Factory FACTORY = new Factory() {
@Override public @Nullable JsonAdapter<?> create( @Override public JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) { Type type, Set<? extends Annotation> annotations, Moshi moshi) {
Type elementType = Types.arrayComponentType(type); Type elementType = Types.arrayComponentType(type);
if (elementType == null) return null; if (elementType == null) return null;
@ -70,8 +69,4 @@ final class ArrayJsonAdapter extends JsonAdapter<Object> {
} }
writer.endArray(); writer.endArray();
} }
@Override public String toString() {
return elementAdapter + ".array()";
}
} }

View file

@ -15,8 +15,6 @@
*/ */
package com.squareup.moshi; package com.squareup.moshi;
import com.squareup.moshi.internal.Util;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass; import java.io.ObjectStreamClass;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.Field; import java.lang.reflect.Field;
@ -79,7 +77,7 @@ abstract class ClassFactory<T> {
// Not the expected version of the Oracle Java library! // Not the expected version of the Oracle Java library!
} }
// Try (post-Gingerbread) Dalvik/libcore's ObjectStreamClass mechanism. // Try Dalvik/libcore's ObjectStreamClass mechanism.
// public class ObjectStreamClass { // public class ObjectStreamClass {
// private static native int getConstructorId(Class<?> c); // private static native int getConstructorId(Class<?> c);
// private static native Object newInstance(Class<?> instantiationClass, int methodId); // private static native Object newInstance(Class<?> instantiationClass, int methodId);
@ -104,32 +102,11 @@ abstract class ClassFactory<T> {
} catch (IllegalAccessException e) { } catch (IllegalAccessException e) {
throw new AssertionError(); throw new AssertionError();
} catch (InvocationTargetException e) { } catch (InvocationTargetException e) {
throw Util.rethrowCause(e); throw new RuntimeException(e);
} catch (NoSuchMethodException ignored) { } catch (NoSuchMethodException ignored) {
// Not the expected version of Dalvik/libcore! // Not the expected version of Dalvik/libcore!
} }
// Try (pre-Gingerbread) Dalvik/libcore's ObjectInputStream mechanism.
// public class ObjectInputStream {
// private static native Object newInstance(
// Class<?> instantiationClass, Class<?> constructorClass);
// }
try {
final Method newInstance = ObjectInputStream.class.getDeclaredMethod(
"newInstance", Class.class, Class.class);
newInstance.setAccessible(true);
return new ClassFactory<T>() {
@SuppressWarnings("unchecked")
@Override public T newInstance() throws InvocationTargetException, IllegalAccessException {
return (T) newInstance.invoke(null, rawType, Object.class);
}
@Override public String toString() {
return rawType.getName();
}
};
} catch (Exception ignored) {
}
throw new IllegalArgumentException("cannot construct instances of " + rawType.getName()); throw new IllegalArgumentException("cannot construct instances of " + rawType.getName());
} }
} }

View file

@ -15,77 +15,48 @@
*/ */
package com.squareup.moshi; package com.squareup.moshi;
import com.squareup.moshi.internal.Util;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import javax.annotation.Nullable;
import static com.squareup.moshi.internal.Util.resolve;
/** /**
* Emits a regular class as a JSON object by mapping Java fields to JSON object properties. * Emits a regular class as a JSON object by mapping Java fields to JSON object properties. Fields
* * of classes in {@code java.*}, {@code javax.*} and {@code android.*} are omitted from both
* <h3>Platform Types</h3> * serialization and deserialization unless they are either public or protected.
* Fields from platform classes are omitted from both serialization and deserialization unless
* they are either public or protected. This includes the following packages and their subpackages:
*
* <ul>
* <li>android.*
* <li>java.*
* <li>javax.*
* <li>kotlin.*
* <li>scala.*
* </ul>
*/ */
final class ClassJsonAdapter<T> extends JsonAdapter<T> { final class ClassJsonAdapter<T> extends JsonAdapter<T> {
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() { public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
@Override public @Nullable JsonAdapter<?> create( @Override public JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) { Type type, Set<? extends Annotation> annotations, Moshi moshi) {
if (!(type instanceof Class) && !(type instanceof ParameterizedType)) {
return null;
}
Class<?> rawType = Types.getRawType(type); Class<?> rawType = Types.getRawType(type);
if (rawType.isInterface() || rawType.isEnum()) return null; if (rawType.isInterface() || rawType.isEnum()) return null;
if (isPlatformType(rawType)) {
throw new IllegalArgumentException("Platform "
+ type
+ " annotated "
+ annotations
+ " requires explicit JsonAdapter to be registered");
}
if (!annotations.isEmpty()) return null; if (!annotations.isEmpty()) return null;
if (Util.isPlatformType(rawType)) {
throw new IllegalArgumentException(
"Platform " + type + " requires explicit JsonAdapter to be registered");
}
if (rawType.isAnonymousClass()) {
throw new IllegalArgumentException("Cannot serialize anonymous class " + rawType.getName());
}
if (rawType.isLocalClass()) {
throw new IllegalArgumentException("Cannot serialize local class " + rawType.getName());
}
if (rawType.getEnclosingClass() != null && !Modifier.isStatic(rawType.getModifiers())) { if (rawType.getEnclosingClass() != null && !Modifier.isStatic(rawType.getModifiers())) {
throw new IllegalArgumentException( if (rawType.getSimpleName().isEmpty()) {
"Cannot serialize non-static nested class " + rawType.getName()); throw new IllegalArgumentException(
"Cannot serialize anonymous class " + rawType.getName());
} else {
throw new IllegalArgumentException(
"Cannot serialize non-static nested class " + rawType.getName());
}
} }
if (Modifier.isAbstract(rawType.getModifiers())) { if (Modifier.isAbstract(rawType.getModifiers())) {
throw new IllegalArgumentException("Cannot serialize abstract class " + rawType.getName()); throw new IllegalArgumentException("Cannot serialize abstract class " + rawType.getName());
} }
try {
//noinspection unchecked if the Class.forName works, the cast will work.
Class<? extends Annotation> metadataClass =
(Class<? extends Annotation>) Class.forName("kotlin.Metadata");
if (rawType.isAnnotationPresent(metadataClass)) {
throw new IllegalArgumentException("Cannot serialize Kotlin type " + rawType.getName()
+ ". Reflective serialization of Kotlin classes without using kotlin-reflect has "
+ "undefined and unexpected behavior. Please use KotlinJsonAdapter from the "
+ "moshi-kotlin artifact or use code gen from the moshi-kotlin-codegen artifact.");
}
} catch (ClassNotFoundException ignored) {
}
ClassFactory<Object> classFactory = ClassFactory.get(rawType); ClassFactory<Object> classFactory = ClassFactory.get(rawType);
Map<String, FieldBinding<?>> fields = new TreeMap<>(); Map<String, FieldBinding<?>> fields = new TreeMap<>();
@ -99,23 +70,22 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
private void createFieldBindings( private void createFieldBindings(
Moshi moshi, Type type, Map<String, FieldBinding<?>> fieldBindings) { Moshi moshi, Type type, Map<String, FieldBinding<?>> fieldBindings) {
Class<?> rawType = Types.getRawType(type); Class<?> rawType = Types.getRawType(type);
boolean platformType = Util.isPlatformType(rawType); boolean platformType = isPlatformType(rawType);
for (Field field : rawType.getDeclaredFields()) { for (Field field : rawType.getDeclaredFields()) {
if (!includeField(platformType, field.getModifiers())) continue; if (!includeField(platformType, field.getModifiers())) continue;
// Look up a type adapter for this type. // Look up a type adapter for this type.
Type fieldType = resolve(type, rawType, field.getGenericType()); Type fieldType = Types.resolve(type, rawType, field.getGenericType());
Set<? extends Annotation> annotations = Util.jsonAnnotations(field); Set<? extends Annotation> annotations = Util.jsonAnnotations(field);
String fieldName = field.getName(); JsonAdapter<Object> adapter = moshi.adapter(fieldType, annotations);
JsonAdapter<Object> adapter = moshi.adapter(fieldType, annotations, fieldName);
// Create the binding between field and JSON. // Create the binding between field and JSON.
field.setAccessible(true); field.setAccessible(true);
FieldBinding<Object> fieldBinding = new FieldBinding<>(field, adapter);
// Store it using the field's name. If there was already a field with this name, fail! // Store it using the field's name. If there was already a field with this name, fail!
Json jsonAnnotation = field.getAnnotation(Json.class); Json jsonAnnotation = field.getAnnotation(Json.class);
String name = jsonAnnotation != null ? jsonAnnotation.name() : fieldName; String name = jsonAnnotation != null ? jsonAnnotation.name() : field.getName();
FieldBinding<Object> fieldBinding = new FieldBinding<>(name, field, adapter);
FieldBinding<?> replaced = fieldBindings.put(name, fieldBinding); FieldBinding<?> replaced = fieldBindings.put(name, fieldBinding);
if (replaced != null) { if (replaced != null) {
throw new IllegalArgumentException("Conflicting fields:\n" throw new IllegalArgumentException("Conflicting fields:\n"
@ -125,22 +95,29 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
} }
} }
/**
* Returns true if {@code rawType} is built in. We don't reflect on private fields of platform
* types because they're unspecified and likely to be different on Java vs. Android.
*/
private boolean isPlatformType(Class<?> rawType) {
return rawType.getName().startsWith("java.")
|| rawType.getName().startsWith("javax.")
|| rawType.getName().startsWith("android.");
}
/** Returns true if fields with {@code modifiers} are included in the emitted JSON. */ /** Returns true if fields with {@code modifiers} are included in the emitted JSON. */
private boolean includeField(boolean platformType, int modifiers) { private boolean includeField(boolean platformType, int modifiers) {
if (Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers)) return false; if (Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers)) return false;
return Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers) || !platformType; return Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers)|| !platformType;
} }
}; };
private final ClassFactory<T> classFactory; private final ClassFactory<T> classFactory;
private final FieldBinding<?>[] fieldsArray; private final Map<String, FieldBinding<?>> jsonFields;
private final JsonReader.Options options;
ClassJsonAdapter(ClassFactory<T> classFactory, Map<String, FieldBinding<?>> fieldsMap) { private ClassJsonAdapter(ClassFactory<T> classFactory, Map<String, FieldBinding<?>> jsonFields) {
this.classFactory = classFactory; this.classFactory = classFactory;
this.fieldsArray = fieldsMap.values().toArray(new FieldBinding[fieldsMap.size()]); this.jsonFields = jsonFields;
this.options = JsonReader.Options.of(
fieldsMap.keySet().toArray(new String[fieldsMap.size()]));
} }
@Override public T fromJson(JsonReader reader) throws IOException { @Override public T fromJson(JsonReader reader) throws IOException {
@ -150,7 +127,10 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
} catch (InstantiationException e) { } catch (InstantiationException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} catch (InvocationTargetException e) { } catch (InvocationTargetException e) {
throw Util.rethrowCause(e); Throwable targetException = e.getTargetException();
if (targetException instanceof RuntimeException) throw (RuntimeException) targetException;
if (targetException instanceof Error) throw (Error) targetException;
throw new RuntimeException(targetException);
} catch (IllegalAccessException e) { } catch (IllegalAccessException e) {
throw new AssertionError(); throw new AssertionError();
} }
@ -158,13 +138,13 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
try { try {
reader.beginObject(); reader.beginObject();
while (reader.hasNext()) { while (reader.hasNext()) {
int index = reader.selectName(options); String name = reader.nextName();
if (index == -1) { FieldBinding<?> fieldBinding = jsonFields.get(name);
reader.skipName(); if (fieldBinding != null) {
fieldBinding.read(reader, result);
} else {
reader.skipValue(); reader.skipValue();
continue;
} }
fieldsArray[index].read(reader, result);
} }
reader.endObject(); reader.endObject();
return result; return result;
@ -176,9 +156,9 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
@Override public void toJson(JsonWriter writer, T value) throws IOException { @Override public void toJson(JsonWriter writer, T value) throws IOException {
try { try {
writer.beginObject(); writer.beginObject();
for (FieldBinding<?> fieldBinding : fieldsArray) { for (Map.Entry<String, FieldBinding<?>> entry : jsonFields.entrySet()) {
writer.name(fieldBinding.name); writer.name(entry.getKey());
fieldBinding.write(writer, value); entry.getValue().write(writer, value);
} }
writer.endObject(); writer.endObject();
} catch (IllegalAccessException e) { } catch (IllegalAccessException e) {
@ -191,23 +171,21 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
} }
static class FieldBinding<T> { static class FieldBinding<T> {
final String name; private final Field field;
final Field field; private final JsonAdapter<T> adapter;
final JsonAdapter<T> adapter;
FieldBinding(String name, Field field, JsonAdapter<T> adapter) { public FieldBinding(Field field, JsonAdapter<T> adapter) {
this.name = name;
this.field = field; this.field = field;
this.adapter = adapter; this.adapter = adapter;
} }
void read(JsonReader reader, Object value) throws IOException, IllegalAccessException { private void read(JsonReader reader, Object value) throws IOException, IllegalAccessException {
T fieldValue = adapter.fromJson(reader); T fieldValue = adapter.fromJson(reader);
field.set(value, fieldValue); field.set(value, fieldValue);
} }
@SuppressWarnings("unchecked") // We require that field's values are of type T. @SuppressWarnings("unchecked") // We require that field's values are of type T.
void write(JsonWriter writer, Object value) throws IllegalAccessException, IOException { private void write(JsonWriter writer, Object value) throws IllegalAccessException, IOException {
T fieldValue = (T) field.get(value); T fieldValue = (T) field.get(value);
adapter.toJson(writer, fieldValue); adapter.toJson(writer, fieldValue);
} }

View file

@ -23,12 +23,11 @@ import java.util.Collection;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import javax.annotation.Nullable;
/** Converts collection types to JSON arrays containing their converted contents. */ /** Converts collection types to JSON arrays containing their converted contents. */
abstract class CollectionJsonAdapter<C extends Collection<T>, T> extends JsonAdapter<C> { abstract class CollectionJsonAdapter<C extends Collection<T>, T> extends JsonAdapter<C> {
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() { public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
@Override public @Nullable JsonAdapter<?> create( @Override public JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) { Type type, Set<? extends Annotation> annotations, Moshi moshi) {
Class<?> rawType = Types.getRawType(type); Class<?> rawType = Types.getRawType(type);
if (!annotations.isEmpty()) return null; if (!annotations.isEmpty()) return null;
@ -47,7 +46,7 @@ abstract class CollectionJsonAdapter<C extends Collection<T>, T> extends JsonAda
this.elementAdapter = elementAdapter; this.elementAdapter = elementAdapter;
} }
static <T> JsonAdapter<Collection<T>> newArrayListAdapter(Type type, Moshi moshi) { private static <T> JsonAdapter<Collection<T>> newArrayListAdapter(Type type, Moshi moshi) {
Type elementType = Types.collectionElementType(type, Collection.class); Type elementType = Types.collectionElementType(type, Collection.class);
JsonAdapter<T> elementAdapter = moshi.adapter(elementType); JsonAdapter<T> elementAdapter = moshi.adapter(elementType);
return new CollectionJsonAdapter<Collection<T>, T>(elementAdapter) { return new CollectionJsonAdapter<Collection<T>, T>(elementAdapter) {
@ -57,7 +56,7 @@ abstract class CollectionJsonAdapter<C extends Collection<T>, T> extends JsonAda
}; };
} }
static <T> JsonAdapter<Set<T>> newLinkedHashSetAdapter(Type type, Moshi moshi) { private static <T> JsonAdapter<Set<T>> newLinkedHashSetAdapter(Type type, Moshi moshi) {
Type elementType = Types.collectionElementType(type, Collection.class); Type elementType = Types.collectionElementType(type, Collection.class);
JsonAdapter<T> elementAdapter = moshi.adapter(elementType); JsonAdapter<T> elementAdapter = moshi.adapter(elementType);
return new CollectionJsonAdapter<Set<T>, T>(elementAdapter) { return new CollectionJsonAdapter<Set<T>, T>(elementAdapter) {

View file

@ -19,23 +19,12 @@ import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME; import static java.lang.annotation.RetentionPolicy.RUNTIME;
/** /** Customizes how a field is encoded as JSON. */
* Customizes how a field is encoded as JSON. @Target({FIELD, METHOD})
*
* <p>Although this annotation doesn't declare a {@link Target}, it is only honored in the following
* elements:
*
* <ul>
* <li><strong>Java class fields</strong>
* <li><strong>Kotlin properties</strong> for use with {@code moshi-kotlin}. This includes both
* properties declared in the constructor and properties declared as members.
* </ul>
*
* <p>Users of the <a href="https://github.com/rharter/auto-value-moshi">AutoValue: Moshi
* Extension</a> may also use this annotation on abstract getters.
*/
@Retention(RUNTIME) @Retention(RUNTIME)
@Documented @Documented
public @interface Json { public @interface Json {

View file

@ -15,14 +15,10 @@
*/ */
package com.squareup.moshi; package com.squareup.moshi;
import com.squareup.moshi.internal.NullSafeJsonAdapter;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.Set; import java.util.Set;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import okio.Buffer; import okio.Buffer;
import okio.BufferedSink; import okio.BufferedSink;
import okio.BufferedSource; import okio.BufferedSource;
@ -31,29 +27,24 @@ import okio.BufferedSource;
* Converts Java values to JSON, and JSON values to Java. * Converts Java values to JSON, and JSON values to Java.
*/ */
public abstract class JsonAdapter<T> { public abstract class JsonAdapter<T> {
@CheckReturnValue public abstract @Nullable T fromJson(JsonReader reader) throws IOException; public abstract T fromJson(JsonReader reader) throws IOException;
@CheckReturnValue public final @Nullable T fromJson(BufferedSource source) throws IOException { public final T fromJson(BufferedSource source) throws IOException {
return fromJson(JsonReader.of(source)); return fromJson(JsonReader.of(source));
} }
@CheckReturnValue public final @Nullable T fromJson(String string) throws IOException { public final T fromJson(String string) throws IOException {
JsonReader reader = JsonReader.of(new Buffer().writeUtf8(string)); return fromJson(new Buffer().writeUtf8(string));
T result = fromJson(reader);
if (!isLenient() && reader.peek() != JsonReader.Token.END_DOCUMENT) {
throw new JsonDataException("JSON document was not fully consumed.");
}
return result;
} }
public abstract void toJson(JsonWriter writer, @Nullable T value) throws IOException; public abstract void toJson(JsonWriter writer, T value) throws IOException;
public final void toJson(BufferedSink sink, @Nullable T value) throws IOException { public final void toJson(BufferedSink sink, T value) throws IOException {
JsonWriter writer = JsonWriter.of(sink); JsonWriter writer = JsonWriter.of(sink);
toJson(writer, value); toJson(writer, value);
} }
@CheckReturnValue public final String toJson(@Nullable T value) { public final String toJson(T value) {
Buffer buffer = new Buffer(); Buffer buffer = new Buffer();
try { try {
toJson(buffer, value); toJson(buffer, value);
@ -63,113 +54,38 @@ public abstract class JsonAdapter<T> {
return buffer.readUtf8(); return buffer.readUtf8();
} }
/**
* Encodes {@code value} as a Java value object comprised of maps, lists, strings, numbers,
* booleans, and nulls.
*
* <p>Values encoded using {@code value(double)} or {@code value(long)} are modeled with the
* corresponding boxed type. Values encoded using {@code value(Number)} are modeled as a
* {@link Long} for boxed integer types ({@link Byte}, {@link Short}, {@link Integer}, and {@link
* Long}), as a {@link Double} for boxed floating point types ({@link Float} and {@link Double}),
* and as a {@link BigDecimal} for all other types.
*/
@CheckReturnValue public final @Nullable Object toJsonValue(@Nullable T value) {
JsonValueWriter writer = new JsonValueWriter();
try {
toJson(writer, value);
return writer.root();
} catch (IOException e) {
throw new AssertionError(e); // No I/O writing to an object.
}
}
/**
* Decodes a Java value object from {@code value}, which must be comprised of maps, lists,
* strings, numbers, booleans and nulls.
*/
@CheckReturnValue public final @Nullable T fromJsonValue(@Nullable Object value) {
JsonValueReader reader = new JsonValueReader(value);
try {
return fromJson(reader);
} catch (IOException e) {
throw new AssertionError(e); // No I/O reading from an object.
}
}
/**
* Returns a JSON adapter equal to this JSON adapter, but that serializes nulls when encoding
* JSON.
*/
@CheckReturnValue public final JsonAdapter<T> serializeNulls() {
final JsonAdapter<T> delegate = this;
return new JsonAdapter<T>() {
@Override public @Nullable T fromJson(JsonReader reader) throws IOException {
return delegate.fromJson(reader);
}
@Override public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
boolean serializeNulls = writer.getSerializeNulls();
writer.setSerializeNulls(true);
try {
delegate.toJson(writer, value);
} finally {
writer.setSerializeNulls(serializeNulls);
}
}
@Override boolean isLenient() {
return delegate.isLenient();
}
@Override public String toString() {
return delegate + ".serializeNulls()";
}
};
}
/** /**
* Returns a JSON adapter equal to this JSON adapter, but with support for reading and writing * Returns a JSON adapter equal to this JSON adapter, but with support for reading and writing
* nulls. * nulls.
*/ */
@CheckReturnValue public final JsonAdapter<T> nullSafe() { public final JsonAdapter<T> nullSafe() {
return new NullSafeJsonAdapter<>(this);
}
/**
* Returns a JSON adapter equal to this JSON adapter, but that refuses null values. If null is
* read or written this will throw a {@link JsonDataException}.
*
* <p>Note that this adapter will not usually be invoked for absent values and so those must be
* handled elsewhere. This should only be used to fail on explicit nulls.
*/
@CheckReturnValue public final JsonAdapter<T> nonNull() {
final JsonAdapter<T> delegate = this; final JsonAdapter<T> delegate = this;
return new JsonAdapter<T>() { return new JsonAdapter<T>() {
@Override public @Nullable T fromJson(JsonReader reader) throws IOException { @Override public T fromJson(JsonReader reader) throws IOException {
if (reader.peek() == JsonReader.Token.NULL) { if (reader.peek() == JsonReader.Token.NULL) {
throw new JsonDataException("Unexpected null at " + reader.getPath()); return reader.nextNull();
} else { } else {
return delegate.fromJson(reader); return delegate.fromJson(reader);
} }
} }
@Override public void toJson(JsonWriter writer, @Nullable T value) throws IOException { @Override public void toJson(JsonWriter writer, T value) throws IOException {
if (value == null) { if (value == null) {
throw new JsonDataException("Unexpected null at " + writer.getPath()); writer.nullValue();
} else { } else {
delegate.toJson(writer, value); delegate.toJson(writer, value);
} }
} }
@Override boolean isLenient() {
return delegate.isLenient();
}
@Override public String toString() { @Override public String toString() {
return delegate + ".nonNull()"; return delegate + ".nullSafe()";
} }
}; };
} }
/** Returns a JSON adapter equal to this, but is lenient when reading and writing. */ /** Returns a JSON adapter equal to this, but is lenient when reading and writing. */
@CheckReturnValue public final JsonAdapter<T> lenient() { public final JsonAdapter<T> lenient() {
final JsonAdapter<T> delegate = this; final JsonAdapter<T> delegate = this;
return new JsonAdapter<T>() { return new JsonAdapter<T>() {
@Override public @Nullable T fromJson(JsonReader reader) throws IOException { @Override public T fromJson(JsonReader reader) throws IOException {
boolean lenient = reader.isLenient(); boolean lenient = reader.isLenient();
reader.setLenient(true); reader.setLenient(true);
try { try {
@ -178,7 +94,7 @@ public abstract class JsonAdapter<T> {
reader.setLenient(lenient); reader.setLenient(lenient);
} }
} }
@Override public void toJson(JsonWriter writer, @Nullable T value) throws IOException { @Override public void toJson(JsonWriter writer, T value) throws IOException {
boolean lenient = writer.isLenient(); boolean lenient = writer.isLenient();
writer.setLenient(true); writer.setLenient(true);
try { try {
@ -187,9 +103,6 @@ public abstract class JsonAdapter<T> {
writer.setLenient(lenient); writer.setLenient(lenient);
} }
} }
@Override boolean isLenient() {
return true;
}
@Override public String toString() { @Override public String toString() {
return delegate + ".lenient()"; return delegate + ".lenient()";
} }
@ -198,14 +111,14 @@ public abstract class JsonAdapter<T> {
/** /**
* Returns a JSON adapter equal to this, but that throws a {@link JsonDataException} when * Returns a JSON adapter equal to this, but that throws a {@link JsonDataException} when
* {@linkplain JsonReader#setFailOnUnknown(boolean) unknown names and values} are encountered. * {@linkplain JsonReader#setFailOnUnknown(boolean) unknown values} are encountered. This
* This constraint applies to both the top-level message handled by this type adapter as well as * constraint applies to both the top-level message handled by this type adapter as well as to
* to nested messages. * nested messages.
*/ */
@CheckReturnValue public final JsonAdapter<T> failOnUnknown() { public final JsonAdapter<T> failOnUnknown() {
final JsonAdapter<T> delegate = this; final JsonAdapter<T> delegate = this;
return new JsonAdapter<T>() { return new JsonAdapter<T>() {
@Override public @Nullable T fromJson(JsonReader reader) throws IOException { @Override public T fromJson(JsonReader reader) throws IOException {
boolean skipForbidden = reader.failOnUnknown(); boolean skipForbidden = reader.failOnUnknown();
reader.setFailOnUnknown(true); reader.setFailOnUnknown(true);
try { try {
@ -214,67 +127,24 @@ public abstract class JsonAdapter<T> {
reader.setFailOnUnknown(skipForbidden); reader.setFailOnUnknown(skipForbidden);
} }
} }
@Override public void toJson(JsonWriter writer, @Nullable T value) throws IOException { @Override public void toJson(JsonWriter writer, T value) throws IOException {
delegate.toJson(writer, value); delegate.toJson(writer, value);
} }
@Override boolean isLenient() {
return delegate.isLenient();
}
@Override public String toString() { @Override public String toString() {
return delegate + ".failOnUnknown()"; return delegate + ".failOnUnknown()";
} }
}; };
} }
/**
* Return a JSON adapter equal to this, but using {@code indent} to control how the result is
* formatted. The {@code indent} string to be repeated for each level of indentation in the
* encoded document. If {@code indent.isEmpty()} the encoded document will be compact. Otherwise
* the encoded document will be more human-readable.
*
* @param indent a string containing only whitespace.
*/
@CheckReturnValue public JsonAdapter<T> indent(final String indent) {
if (indent == null) {
throw new NullPointerException("indent == null");
}
final JsonAdapter<T> delegate = this;
return new JsonAdapter<T>() {
@Override public @Nullable T fromJson(JsonReader reader) throws IOException {
return delegate.fromJson(reader);
}
@Override public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
String originalIndent = writer.getIndent();
writer.setIndent(indent);
try {
delegate.toJson(writer, value);
} finally {
writer.setIndent(originalIndent);
}
}
@Override boolean isLenient() {
return delegate.isLenient();
}
@Override public String toString() {
return delegate + ".indent(\"" + indent + "\")";
}
};
}
boolean isLenient() {
return false;
}
public interface Factory { public interface Factory {
/** /**
* Attempts to create an adapter for {@code type} annotated with {@code annotations}. This * Attempts to create an adapter for {@code type} annotated with {@code annotations}. This
* returns the adapter if one was created, or null if this factory isn't capable of creating * returns the adapter if one was created, or null if this factory isn't capable of creating
* such an adapter. * such an adapter.
* *
* <p>Implementations may use {@link Moshi#adapter} to compose adapters of other types, or * <p>Implementations may use to {@link Moshi#adapter} to compose adapters of other types, or
* {@link Moshi#nextAdapter} to delegate to the underlying adapter of the same type. * {@link Moshi#nextAdapter} to delegate to the underlying adapter of the same type.
*/ */
@CheckReturnValue JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi);
@Nullable JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi);
} }
} }

View file

@ -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 Moshis built-in
* generator to create the API signature to get started, then make your own generator match that
* expected signature.
*/
String generator() default "";
}

View file

@ -15,8 +15,6 @@
*/ */
package com.squareup.moshi; package com.squareup.moshi;
import javax.annotation.Nullable;
/** /**
* Thrown when the data in a JSON document doesn't match the data expected by the caller. For * Thrown when the data in a JSON document doesn't match the data expected by the caller. For
* example, suppose the application expects a boolean but the JSON document contains a string. When * example, suppose the application expects a boolean but the JSON document contains a string. When
@ -24,24 +22,20 @@ import javax.annotation.Nullable;
* *
* <p>Exceptions of this type should be fixed by either changing the application code to accept * <p>Exceptions of this type should be fixed by either changing the application code to accept
* the unexpected JSON, or by changing the JSON to conform to the application's expectations. * the unexpected JSON, or by changing the JSON to conform to the application's expectations.
*
* <p>This exception may also be triggered if a document's nesting exceeds 31 levels. This depth is
* sufficient for all practical applications, but shallow enough to avoid uglier failures like
* {@link StackOverflowError}.
*/ */
public final class JsonDataException extends RuntimeException { public final class JsonDataException extends RuntimeException {
public JsonDataException() { public JsonDataException() {
} }
public JsonDataException(@Nullable String message) { public JsonDataException(String message) {
super(message); super(message);
} }
public JsonDataException(@Nullable Throwable cause) { public JsonDataException(Throwable cause) {
super(cause); super(cause);
} }
public JsonDataException(@Nullable String message, @Nullable Throwable cause) { public JsonDataException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }
} }

View file

@ -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

View file

@ -17,8 +17,6 @@ package com.squareup.moshi;
/** Lexical scoping elements within a JSON reader or writer. */ /** Lexical scoping elements within a JSON reader or writer. */
final class JsonScope { final class JsonScope {
private JsonScope() {
}
/** An array with no elements requires no separators or newlines before it is closed. */ /** An array with no elements requires no separators or newlines before it is closed. */
static final int EMPTY_ARRAY = 1; static final int EMPTY_ARRAY = 1;
@ -54,7 +52,7 @@ final class JsonScope {
*/ */
static String getPath(int stackSize, int[] stack, String[] pathNames, int[] pathIndices) { static String getPath(int stackSize, int[] stack, String[] pathNames, int[] pathIndices) {
StringBuilder result = new StringBuilder().append('$'); StringBuilder result = new StringBuilder().append('$');
for (int i = 0; i < stackSize; i++) { for (int i = 0, size = stackSize; i < size; i++) {
switch (stack[i]) { switch (stack[i]) {
case EMPTY_ARRAY: case EMPTY_ARRAY:
case NONEMPTY_ARRAY: case NONEMPTY_ARRAY:

File diff suppressed because it is too large Load diff

View file

@ -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.");
}
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -18,19 +18,19 @@ package com.squareup.moshi;
import java.io.Closeable; import java.io.Closeable;
import java.io.Flushable; import java.io.Flushable;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import okio.BufferedSink; import okio.BufferedSink;
import okio.BufferedSource; import okio.Sink;
import static com.squareup.moshi.JsonScope.DANGLING_NAME;
import static com.squareup.moshi.JsonScope.EMPTY_ARRAY; import static com.squareup.moshi.JsonScope.EMPTY_ARRAY;
import static com.squareup.moshi.JsonScope.EMPTY_DOCUMENT;
import static com.squareup.moshi.JsonScope.EMPTY_OBJECT; import static com.squareup.moshi.JsonScope.EMPTY_OBJECT;
import static com.squareup.moshi.JsonScope.NONEMPTY_ARRAY; import static com.squareup.moshi.JsonScope.NONEMPTY_ARRAY;
import static com.squareup.moshi.JsonScope.NONEMPTY_DOCUMENT;
import static com.squareup.moshi.JsonScope.NONEMPTY_OBJECT; import static com.squareup.moshi.JsonScope.NONEMPTY_OBJECT;
/** /**
* Writes a JSON (<a href="http://www.ietf.org/rfc/rfc7159.txt">RFC 7159</a>) * Writes a JSON (<a href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>)
* encoded value to a stream, one token at a time. The stream includes both * encoded value to a stream, one token at a time. The stream includes both
* literal values (strings, numbers, booleans and nulls) as well as the begin * literal values (strings, numbers, booleans and nulls) as well as the begin
* and end delimiters of objects and arrays. * and end delimiters of objects and arrays.
@ -77,7 +77,7 @@ import static com.squareup.moshi.JsonScope.NONEMPTY_OBJECT;
* This code encodes the above structure: <pre> {@code * This code encodes the above structure: <pre> {@code
* public void writeJsonStream(BufferedSink sink, List<Message> messages) throws IOException { * public void writeJsonStream(BufferedSink sink, List<Message> messages) throws IOException {
* JsonWriter writer = JsonWriter.of(sink); * JsonWriter writer = JsonWriter.of(sink);
* writer.setIndent(" "); * writer.setIndentSpaces(4);
* writeMessagesArray(writer, messages); * writeMessagesArray(writer, messages);
* writer.close(); * writer.close();
* } * }
@ -124,87 +124,76 @@ import static com.squareup.moshi.JsonScope.NONEMPTY_OBJECT;
* Instances of this class are not thread safe. Calls that would result in a * Instances of this class are not thread safe. Calls that would result in a
* malformed JSON string will fail with an {@link IllegalStateException}. * malformed JSON string will fail with an {@link IllegalStateException}.
*/ */
public abstract class JsonWriter implements Closeable, Flushable { public class JsonWriter implements Closeable, Flushable {
// The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack will
// grow itself up to 256 levels of nesting including the top-level document. Deeper nesting is /*
// prone to trigger StackOverflowErrors. * From RFC 4627, "All Unicode characters may be placed within the
int stackSize = 0; * quotation marks except for the characters that must be escaped:
int[] scopes = new int[32]; * quotation mark, reverse solidus, and the control characters
String[] pathNames = new String[32]; * (U+0000 through U+001F)."
int[] pathIndices = new int[32]; *
* We also escape '\u2028' and '\u2029', which JavaScript interprets as
* newline characters. This prevents eval() from failing with a syntax
* error. http://code.google.com/p/google-gson/issues/detail?id=341
*/
private static final String[] REPLACEMENT_CHARS;
static {
REPLACEMENT_CHARS = new String[128];
for (int i = 0; i <= 0x1f; i++) {
REPLACEMENT_CHARS[i] = String.format("\\u%04x", (int) i);
}
REPLACEMENT_CHARS['"'] = "\\\"";
REPLACEMENT_CHARS['\\'] = "\\\\";
REPLACEMENT_CHARS['\t'] = "\\t";
REPLACEMENT_CHARS['\b'] = "\\b";
REPLACEMENT_CHARS['\n'] = "\\n";
REPLACEMENT_CHARS['\r'] = "\\r";
REPLACEMENT_CHARS['\f'] = "\\f";
}
/** The output data, containing at most one top-level array or object. */
private final BufferedSink sink;
private int[] stack = new int[32];
private int stackSize = 0;
{
push(EMPTY_DOCUMENT);
}
private String[] pathNames = new String[32];
private int[] pathIndices = new int[32];
/** /**
* A string containing a full set of spaces for a single level of indentation, or null for no * A string containing a full set of spaces for a single level of
* pretty printing. * indentation, or null for no pretty printing.
*/ */
String indent; private String indent;
boolean lenient;
boolean serializeNulls;
boolean promoteValueToName;
/** /**
* Controls the deepest stack size that has begin/end pairs flattened: * The name/value separator; either ":" or ": ".
*
* <ul>
* <li>If -1, no begin/end pairs are being suppressed.
* <li>If positive, this is the deepest stack size whose begin/end pairs are eligible to be
* flattened.
* <li>If negative, it is the bitwise inverse (~) of the deepest stack size whose begin/end
* pairs have been flattened.
* </ul>
*
* <p>We differentiate between what layer would be flattened (positive) from what layer is being
* flattened (negative) so that we don't double-flatten.
*
* <p>To accommodate nested flattening we require callers to track the previous state when they
* provide a new state. The previous state is returned from {@link #beginFlatten} and restored
* with {@link #endFlatten}.
*/ */
int flattenStackSize = -1; private String separator = ":";
/** Returns a new instance that writes UTF-8 encoded JSON to {@code sink}. */ private boolean lenient;
@CheckReturnValue public static JsonWriter of(BufferedSink sink) {
return new JsonUtf8Writer(sink);
}
JsonWriter() { private String deferredName;
// Package-private to control subclasses.
}
/** Returns the scope on the top of the stack. */ private boolean serializeNulls;
final int peekScope() {
if (stackSize == 0) { private boolean promoteNameToValue;
throw new IllegalStateException("JsonWriter is closed.");
private JsonWriter(BufferedSink sink) {
if (sink == null) {
throw new NullPointerException("sink == null");
} }
return scopes[stackSize - 1]; this.sink = sink;
} }
/** Before pushing a value on the stack this confirms that the stack has capacity. */ /**
final boolean checkStack() { * Returns a new instance that writes a JSON-encoded stream to {@code sink}.
if (stackSize != scopes.length) return false; */
public static JsonWriter of(BufferedSink sink) {
if (stackSize == 256) { return new JsonWriter(sink);
throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?");
}
scopes = Arrays.copyOf(scopes, scopes.length * 2);
pathNames = Arrays.copyOf(pathNames, pathNames.length * 2);
pathIndices = Arrays.copyOf(pathIndices, pathIndices.length * 2);
if (this instanceof JsonValueWriter) {
((JsonValueWriter) this).stack =
Arrays.copyOf(((JsonValueWriter) this).stack, ((JsonValueWriter) this).stack.length * 2);
}
return true;
}
final void pushScope(int newTop) {
scopes[stackSize++] = newTop;
}
/** Replace the value on the top of the stack with the given value. */
final void replaceTop(int topOfStack) {
scopes[stackSize - 1] = topOfStack;
} }
/** /**
@ -215,22 +204,20 @@ public abstract class JsonWriter implements Closeable, Flushable {
* *
* @param indent a string containing only whitespace. * @param indent a string containing only whitespace.
*/ */
public void setIndent(String indent) { public final void setIndent(String indent) {
this.indent = !indent.isEmpty() ? indent : null; if (indent.length() == 0) {
} this.indent = null;
this.separator = ":";
/** } else {
* Returns a string containing only whitespace, used for each level of this.indent = indent;
* indentation. If empty, the encoded document will be compact. this.separator = ": ";
*/ }
@CheckReturnValue public final String getIndent() {
return indent != null ? indent : "";
} }
/** /**
* Configure this writer to relax its syntax rules. By default, this writer * Configure this writer to relax its syntax rules. By default, this writer
* only emits well-formed JSON as specified by <a * only emits well-formed JSON as specified by <a
* href="http://www.ietf.org/rfc/rfc7159.txt">RFC 7159</a>. Setting the writer * href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>. Setting the writer
* to lenient permits the following: * to lenient permits the following:
* <ul> * <ul>
* <li>Top-level values of any type. With strict writing, the top-level * <li>Top-level values of any type. With strict writing, the top-level
@ -246,7 +233,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
/** /**
* Returns true if this writer has relaxed syntax rules. * Returns true if this writer has relaxed syntax rules.
*/ */
@CheckReturnValue public final boolean isLenient() { public boolean isLenient() {
return lenient; return lenient;
} }
@ -262,7 +249,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
* Returns true if object members are serialized when their value is null. * Returns true if object members are serialized when their value is null.
* This has no impact on array elements. The default is false. * This has no impact on array elements. The default is false.
*/ */
@CheckReturnValue public final boolean getSerializeNulls() { public final boolean getSerializeNulls() {
return serializeNulls; return serializeNulls;
} }
@ -272,14 +259,19 @@ public abstract class JsonWriter implements Closeable, Flushable {
* *
* @return this writer. * @return this writer.
*/ */
public abstract JsonWriter beginArray() throws IOException; public JsonWriter beginArray() throws IOException {
writeDeferredName();
return open(EMPTY_ARRAY, "[");
}
/** /**
* Ends encoding the current array. * Ends encoding the current array.
* *
* @return this writer. * @return this writer.
*/ */
public abstract JsonWriter endArray() throws IOException; public JsonWriter endArray() throws IOException {
return close(EMPTY_ARRAY, NONEMPTY_ARRAY, "]");
}
/** /**
* Begins encoding a new object. Each call to this method must be paired * Begins encoding a new object. Each call to this method must be paired
@ -287,22 +279,112 @@ public abstract class JsonWriter implements Closeable, Flushable {
* *
* @return this writer. * @return this writer.
*/ */
public abstract JsonWriter beginObject() throws IOException; public JsonWriter beginObject() throws IOException {
writeDeferredName();
return open(EMPTY_OBJECT, "{");
}
/** /**
* Ends encoding the current object. * Ends encoding the current object.
* *
* @return this writer. * @return this writer.
*/ */
public abstract JsonWriter endObject() throws IOException; public JsonWriter endObject() throws IOException {
promoteNameToValue = false;
return close(EMPTY_OBJECT, NONEMPTY_OBJECT, "}");
}
/**
* Enters a new scope by appending any necessary whitespace and the given
* bracket.
*/
private JsonWriter open(int empty, String openBracket) throws IOException {
beforeValue(true);
pathIndices[stackSize] = 0;
push(empty);
sink.writeUtf8(openBracket);
return this;
}
/**
* Closes the current scope by appending any necessary whitespace and the
* given bracket.
*/
private JsonWriter close(int empty, int nonempty, String closeBracket)
throws IOException {
int context = peek();
if (context != nonempty && context != empty) {
throw new IllegalStateException("Nesting problem.");
}
if (deferredName != null) {
throw new IllegalStateException("Dangling name: " + deferredName);
}
stackSize--;
pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected!
pathIndices[stackSize - 1]++;
if (context == nonempty) {
newline();
}
sink.writeUtf8(closeBracket);
return this;
}
private void push(int newTop) {
if (stackSize == stack.length) {
int[] newStack = new int[stackSize * 2];
System.arraycopy(stack, 0, newStack, 0, stackSize);
stack = newStack;
}
stack[stackSize++] = newTop;
}
/**
* Returns the value on the top of the stack.
*/
private int peek() {
if (stackSize == 0) {
throw new IllegalStateException("JsonWriter is closed.");
}
return stack[stackSize - 1];
}
/**
* Replace the value on the top of the stack with the given value.
*/
private void replaceTop(int topOfStack) {
stack[stackSize - 1] = topOfStack;
}
/** /**
* Encodes the property name. * Encodes the property name.
* *
* @param name the name of the forthcoming value. Must not be null. * @param name the name of the forthcoming value. May not be null.
* @return this writer. * @return this writer.
*/ */
public abstract JsonWriter name(String name) throws IOException; public JsonWriter name(String name) throws IOException {
if (name == null) {
throw new NullPointerException("name == null");
}
if (stackSize == 0) {
throw new IllegalStateException("JsonWriter is closed.");
}
if (deferredName != null) {
throw new IllegalStateException();
}
deferredName = name;
pathNames[stackSize - 1] = name;
promoteNameToValue = false;
return this;
}
private void writeDeferredName() throws IOException {
if (deferredName != null) {
beforeName();
string(deferredName);
deferredName = null;
}
}
/** /**
* Encodes {@code value}. * Encodes {@code value}.
@ -310,28 +392,52 @@ public abstract class JsonWriter implements Closeable, Flushable {
* @param value the literal string value, or null to encode a null literal. * @param value the literal string value, or null to encode a null literal.
* @return this writer. * @return this writer.
*/ */
public abstract JsonWriter value(@Nullable String value) throws IOException; public JsonWriter value(String value) throws IOException {
if (value == null) {
return nullValue();
}
if (promoteNameToValue) {
return name(value);
}
writeDeferredName();
beforeValue(false);
string(value);
pathIndices[stackSize - 1]++;
return this;
}
/** /**
* Encodes {@code null}. * Encodes {@code null}.
* *
* @return this writer. * @return this writer.
*/ */
public abstract JsonWriter nullValue() throws IOException; public JsonWriter nullValue() throws IOException {
if (deferredName != null) {
if (serializeNulls) {
writeDeferredName();
} else {
deferredName = null;
return this; // skip the name and the value
}
}
beforeValue(false);
sink.writeUtf8("null");
pathIndices[stackSize - 1]++;
return this;
}
/** /**
* Encodes {@code value}. * Encodes {@code value}.
* *
* @return this writer. * @return this writer.
*/ */
public abstract JsonWriter value(boolean value) throws IOException; public JsonWriter value(boolean value) throws IOException {
writeDeferredName();
/** beforeValue(false);
* Encodes {@code value}. sink.writeUtf8(value ? "true" : "false");
* pathIndices[stackSize - 1]++;
* @return this writer. return this;
*/ }
public abstract JsonWriter value(@Nullable Boolean value) throws IOException;
/** /**
* Encodes {@code value}. * Encodes {@code value}.
@ -340,14 +446,35 @@ public abstract class JsonWriter implements Closeable, Flushable {
* {@linkplain Double#isInfinite() infinities}. * {@linkplain Double#isInfinite() infinities}.
* @return this writer. * @return this writer.
*/ */
public abstract JsonWriter value(double value) throws IOException; public JsonWriter value(double value) throws IOException {
if (Double.isNaN(value) || Double.isInfinite(value)) {
throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
}
if (promoteNameToValue) {
return name(Double.toString(value));
}
writeDeferredName();
beforeValue(false);
sink.writeUtf8(Double.toString(value));
pathIndices[stackSize - 1]++;
return this;
}
/** /**
* Encodes {@code value}. * Encodes {@code value}.
* *
* @return this writer. * @return this writer.
*/ */
public abstract JsonWriter value(long value) throws IOException; public JsonWriter value(long value) throws IOException {
if (promoteNameToValue) {
return name(Long.toString(value));
}
writeDeferredName();
beforeValue(false);
sink.writeUtf8(Long.toString(value));
pathIndices[stackSize - 1]++;
return this;
}
/** /**
* Encodes {@code value}. * Encodes {@code value}.
@ -356,116 +483,172 @@ public abstract class JsonWriter implements Closeable, Flushable {
* {@linkplain Double#isInfinite() infinities}. * {@linkplain Double#isInfinite() infinities}.
* @return this writer. * @return this writer.
*/ */
public abstract JsonWriter value(@Nullable Number value) throws IOException; public JsonWriter value(Number value) throws IOException {
if (value == null) {
return nullValue();
}
String string = value.toString();
if (!lenient
&& (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN"))) {
throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
}
if (promoteNameToValue) {
return name(string);
}
writeDeferredName();
beforeValue(false);
sink.writeUtf8(string);
pathIndices[stackSize - 1]++;
return this;
}
/** /**
* Writes {@code source} directly without encoding its contents. * Ensures all buffered data is written to the underlying {@link Sink}
* Since no validation is performed, {@link #setSerializeNulls} and other writer configurations * and flushes that writer.
* are not respected. */
public void flush() throws IOException {
if (stackSize == 0) {
throw new IllegalStateException("JsonWriter is closed.");
}
sink.flush();
}
/**
* Flushes and closes this writer and the underlying {@link Sink}.
* *
* @return this writer. * @throws JsonDataException if the JSON document is incomplete.
*/ */
public abstract JsonWriter value(BufferedSource source) throws IOException; public void close() throws IOException {
sink.close();
int size = stackSize;
if (size > 1 || size == 1 && stack[size - 1] != NONEMPTY_DOCUMENT) {
throw new IOException("Incomplete document");
}
stackSize = 0;
}
private void string(String value) throws IOException {
String[] replacements = REPLACEMENT_CHARS;
sink.writeByte('"');
int last = 0;
int length = value.length();
for (int i = 0; i < length; i++) {
char c = value.charAt(i);
String replacement;
if (c < 128) {
replacement = replacements[c];
if (replacement == null) {
continue;
}
} else if (c == '\u2028') {
replacement = "\\u2028";
} else if (c == '\u2029') {
replacement = "\\u2029";
} else {
continue;
}
if (last < i) {
sink.writeUtf8(value, last, i);
}
sink.writeUtf8(replacement);
last = i + 1;
}
if (last < length) {
sink.writeUtf8(value, last, length);
}
sink.writeByte('"');
}
private void newline() throws IOException {
if (indent == null) {
return;
}
sink.writeByte('\n');
for (int i = 1, size = stackSize; i < size; i++) {
sink.writeUtf8(indent);
}
}
/** /**
* Changes the writer to treat the next value as a string name. This is useful for map adapters so * Inserts any necessary separators and whitespace before a name. Also
* that arbitrary type adapters can use {@link #value} to write a name value. * adjusts the stack to expect the name's value.
*/ */
final void promoteValueToName() throws IOException { private void beforeName() throws IOException {
int context = peekScope(); int context = peek();
if (context == NONEMPTY_OBJECT) { // first in object
sink.writeByte(',');
} else if (context != EMPTY_OBJECT) { // not in an object!
throw new IllegalStateException("Nesting problem.");
}
newline();
replaceTop(DANGLING_NAME);
}
/**
* Inserts any necessary separators and whitespace before a literal value,
* inline array, or inline object. Also adjusts the stack to expect either a
* closing bracket or another element.
*
* @param root true if the value is a new array or object, the two values
* permitted as top-level elements.
*/
@SuppressWarnings("fallthrough")
private void beforeValue(boolean root) throws IOException {
switch (peek()) {
case NONEMPTY_DOCUMENT:
if (!lenient) {
throw new IllegalStateException(
"JSON must have only one top-level value.");
}
// fall-through
case EMPTY_DOCUMENT: // first in document
if (!lenient && !root) {
throw new IllegalStateException(
"JSON must start with an array or an object.");
}
replaceTop(NONEMPTY_DOCUMENT);
break;
case EMPTY_ARRAY: // first in array
replaceTop(NONEMPTY_ARRAY);
newline();
break;
case NONEMPTY_ARRAY: // another in array
sink.writeByte(',');
newline();
break;
case DANGLING_NAME: // value for name
sink.writeUtf8(separator);
replaceTop(NONEMPTY_OBJECT);
break;
default:
throw new IllegalStateException("Nesting problem.");
}
}
/**
* Changes the reader to treat the next string value as a name. This is useful for map adapters so
* that arbitrary type adapters can use {@link #value(String)} to write a name value.
*/
void promoteNameToValue() throws IOException {
int context = peek();
if (context != NONEMPTY_OBJECT && context != EMPTY_OBJECT) { if (context != NONEMPTY_OBJECT && context != EMPTY_OBJECT) {
throw new IllegalStateException("Nesting problem."); throw new IllegalStateException("Nesting problem.");
} }
promoteValueToName = true; promoteNameToValue = true;
}
/**
* Cancels immediately-nested calls to {@link #beginArray()} or {@link #beginObject()} and their
* matching calls to {@link #endArray} or {@link #endObject()}. Use this to compose JSON adapters
* without nesting.
*
* <p>For example, the following creates JSON with nested arrays: {@code [1,[2,3,4],5]}.
*
* <pre>{@code
*
* JsonAdapter<List<Integer>> integersAdapter = ...
*
* public void writeNumbers(JsonWriter writer) {
* writer.beginArray();
* writer.value(1);
* integersAdapter.toJson(writer, Arrays.asList(2, 3, 4));
* writer.value(5);
* writer.endArray();
* }
* }</pre>
*
* <p>With flattening we can create JSON with a single array {@code [1,2,3,4,5]}:
*
* <pre>{@code
*
* JsonAdapter<List<Integer>> integersAdapter = ...
*
* public void writeNumbers(JsonWriter writer) {
* writer.beginArray();
* int token = writer.beginFlatten();
* writer.value(1);
* integersAdapter.toJson(writer, Arrays.asList(2, 3, 4));
* writer.value(5);
* writer.endFlatten(token);
* writer.endArray();
* }
* }</pre>
*
* <p>This method flattens arrays within arrays:
*
* <pre>{@code
*
* Emit: [1, [2, 3, 4], 5]
* To produce: [1, 2, 3, 4, 5]
* }</pre>
*
* It also flattens objects within objects. Do not call {@link #name} before writing a flattened
* object.
*
* <pre>{@code
*
* Emit: {"a": 1, {"b": 2}, "c": 3}
* To Produce: {"a": 1, "b": 2, "c": 3}
* }</pre>
*
* Other combinations are permitted but do not perform flattening. For example, objects inside of
* arrays are not flattened:
*
* <pre>{@code
*
* Emit: [1, {"b": 2}, 3, [4, 5], 6]
* To Produce: [1, {"b": 2}, 3, 4, 5, 6]
* }</pre>
*
* <p>This method returns an opaque token. Callers must match all calls to this method with a call
* to {@link #endFlatten} with the matching token.
*/
@CheckReturnValue public final int beginFlatten() {
int context = peekScope();
if (context != NONEMPTY_OBJECT && context != EMPTY_OBJECT
&& context != NONEMPTY_ARRAY && context != EMPTY_ARRAY) {
throw new IllegalStateException("Nesting problem.");
}
int token = flattenStackSize;
flattenStackSize = stackSize;
return token;
}
/** Ends nested call flattening created by {@link #beginFlatten}. */
public final void endFlatten(int token) {
flattenStackSize = token;
} }
/** /**
* Returns a <a href="http://goessner.net/articles/JsonPath/">JsonPath</a> to * Returns a <a href="http://goessner.net/articles/JsonPath/">JsonPath</a> to
* the current location in the JSON value. * the current location in the JSON value.
*/ */
@CheckReturnValue public final String getPath() { public String getPath() {
return JsonScope.getPath(stackSize, scopes, pathNames, pathIndices); return JsonScope.getPath(stackSize, stack, pathNames, pathIndices);
} }
} }

View file

@ -55,7 +55,7 @@ final class LinkedHashTreeMap<K, V> extends AbstractMap<K, V> implements Seriali
* Create a natural order, empty tree map whose keys must be mutually * Create a natural order, empty tree map whose keys must be mutually
* comparable and non-null. * comparable and non-null.
*/ */
LinkedHashTreeMap() { public LinkedHashTreeMap() {
this(null); this(null);
} }
@ -66,10 +66,8 @@ final class LinkedHashTreeMap<K, V> extends AbstractMap<K, V> implements Seriali
* @param comparator the comparator to order elements with, or {@code null} to * @param comparator the comparator to order elements with, or {@code null} to
* use the natural ordering. * use the natural ordering.
*/ */
@SuppressWarnings({ @SuppressWarnings({ "unchecked", "rawtypes" }) // unsafe! if comparator is null, this assumes K is comparable
"unchecked", "rawtypes" // Unsafe! if comparator is null, this assumes K is comparable. public LinkedHashTreeMap(Comparator<? super K> comparator) {
})
LinkedHashTreeMap(Comparator<? super K> comparator) {
this.comparator = comparator != null this.comparator = comparator != null
? comparator ? comparator
: (Comparator) NATURAL_ORDER; : (Comparator) NATURAL_ORDER;
@ -475,14 +473,14 @@ final class LinkedHashTreeMap<K, V> extends AbstractMap<K, V> implements Seriali
V value; V value;
int height; int height;
/** Create the header entry. */ /** Create the header entry */
Node() { Node() {
key = null; key = null;
hash = -1; hash = -1;
next = prev = this; next = prev = this;
} }
/** Create a regular entry. */ /** Create a regular entry */
Node(Node<K, V> parent, K key, int hash, Node<K, V> next, Node<K, V> prev) { Node(Node<K, V> parent, K key, int hash, Node<K, V> next, Node<K, V> prev) {
this.parent = parent; this.parent = parent;
this.key = key; this.key = key;
@ -667,7 +665,7 @@ final class LinkedHashTreeMap<K, V> extends AbstractMap<K, V> implements Seriali
* comparisons. Using this class to create a tree of size <i>S</i> is * comparisons. Using this class to create a tree of size <i>S</i> is
* {@code O(S)}. * {@code O(S)}.
*/ */
static final class AvlBuilder<K, V> { final static class AvlBuilder<K, V> {
/** This stack is a singly linked list, linked by the 'parent' field. */ /** This stack is a singly linked list, linked by the 'parent' field. */
private Node<K, V> stack; private Node<K, V> stack;
private int leavesToSkip; private int leavesToSkip;
@ -757,7 +755,7 @@ final class LinkedHashTreeMap<K, V> extends AbstractMap<K, V> implements Seriali
} }
} }
abstract class LinkedTreeMapIterator<T> implements Iterator<T> { private abstract class LinkedTreeMapIterator<T> implements Iterator<T> {
Node<K, V> next = header.next; Node<K, V> next = header.next;
Node<K, V> lastReturned = null; Node<K, V> lastReturned = null;
int expectedModCount = modCount; int expectedModCount = modCount;

View file

@ -20,7 +20,6 @@ import java.lang.annotation.Annotation;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import javax.annotation.Nullable;
/** /**
* Converts maps with string keys to JSON objects. * Converts maps with string keys to JSON objects.
@ -29,7 +28,7 @@ import javax.annotation.Nullable;
*/ */
final class MapJsonAdapter<K, V> extends JsonAdapter<Map<K, V>> { final class MapJsonAdapter<K, V> extends JsonAdapter<Map<K, V>> {
public static final Factory FACTORY = new Factory() { public static final Factory FACTORY = new Factory() {
@Override public @Nullable JsonAdapter<?> create( @Override public JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) { Type type, Set<? extends Annotation> annotations, Moshi moshi) {
if (!annotations.isEmpty()) return null; if (!annotations.isEmpty()) return null;
Class<?> rawType = Types.getRawType(type); Class<?> rawType = Types.getRawType(type);
@ -42,7 +41,7 @@ final class MapJsonAdapter<K, V> extends JsonAdapter<Map<K, V>> {
private final JsonAdapter<K> keyAdapter; private final JsonAdapter<K> keyAdapter;
private final JsonAdapter<V> valueAdapter; private final JsonAdapter<V> valueAdapter;
MapJsonAdapter(Moshi moshi, Type keyType, Type valueType) { public MapJsonAdapter(Moshi moshi, Type keyType, Type valueType) {
this.keyAdapter = moshi.adapter(keyType); this.keyAdapter = moshi.adapter(keyType);
this.valueAdapter = moshi.adapter(valueType); this.valueAdapter = moshi.adapter(valueType);
} }
@ -51,9 +50,9 @@ final class MapJsonAdapter<K, V> extends JsonAdapter<Map<K, V>> {
writer.beginObject(); writer.beginObject();
for (Map.Entry<K, V> entry : map.entrySet()) { for (Map.Entry<K, V> entry : map.entrySet()) {
if (entry.getKey() == null) { if (entry.getKey() == null) {
throw new JsonDataException("Map key is null at " + writer.getPath()); throw new JsonDataException("Map key is null at path " + writer.getPath());
} }
writer.promoteValueToName(); writer.promoteNameToValue();
keyAdapter.toJson(writer, entry.getKey()); keyAdapter.toJson(writer, entry.getKey());
valueAdapter.toJson(writer, entry.getValue()); valueAdapter.toJson(writer, entry.getValue());
} }
@ -70,7 +69,7 @@ final class MapJsonAdapter<K, V> extends JsonAdapter<Map<K, V>> {
V replaced = result.put(name, value); V replaced = result.put(name, value);
if (replaced != null) { if (replaced != null) {
throw new JsonDataException("Map key '" + name + "' has multiple values at path " throw new JsonDataException("Map key '" + name + "' has multiple values at path "
+ reader.getPath() + ": " + replaced + " and " + value); + reader.getPath());
} }
} }
reader.endObject(); reader.endObject();

View file

@ -15,106 +15,47 @@
*/ */
package com.squareup.moshi; package com.squareup.moshi;
import com.squareup.moshi.internal.Util;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import static com.squareup.moshi.internal.Util.canonicalize;
import static com.squareup.moshi.internal.Util.removeSubtypeWildcard;
import static com.squareup.moshi.internal.Util.typeAnnotatedWithAnnotations;
/** /**
* Coordinates binding between JSON values and Java objects. * Coordinates binding between JSON values and Java objects.
*/ */
public final class Moshi { public final class Moshi {
static final List<JsonAdapter.Factory> BUILT_IN_FACTORIES = new ArrayList<>(5);
static {
BUILT_IN_FACTORIES.add(StandardJsonAdapters.FACTORY);
BUILT_IN_FACTORIES.add(CollectionJsonAdapter.FACTORY);
BUILT_IN_FACTORIES.add(MapJsonAdapter.FACTORY);
BUILT_IN_FACTORIES.add(ArrayJsonAdapter.FACTORY);
BUILT_IN_FACTORIES.add(ClassJsonAdapter.FACTORY);
}
private final List<JsonAdapter.Factory> factories; private final List<JsonAdapter.Factory> factories;
private final ThreadLocal<LookupChain> lookupChainThreadLocal = new ThreadLocal<>(); private final ThreadLocal<List<DeferredAdapter<?>>> reentrantCalls = new ThreadLocal<>();
private final Map<Object, JsonAdapter<?>> adapterCache = new LinkedHashMap<>(); private final Map<Object, JsonAdapter<?>> adapterCache = new LinkedHashMap<>();
Moshi(Builder builder) { private Moshi(Builder builder) {
List<JsonAdapter.Factory> factories = new ArrayList<>( List<JsonAdapter.Factory> factories = new ArrayList<>();
builder.factories.size() + BUILT_IN_FACTORIES.size());
factories.addAll(builder.factories); factories.addAll(builder.factories);
factories.addAll(BUILT_IN_FACTORIES); factories.add(StandardJsonAdapters.FACTORY);
factories.add(CollectionJsonAdapter.FACTORY);
factories.add(MapJsonAdapter.FACTORY);
factories.add(ArrayJsonAdapter.FACTORY);
factories.add(ClassJsonAdapter.FACTORY);
this.factories = Collections.unmodifiableList(factories); this.factories = Collections.unmodifiableList(factories);
} }
/** Returns a JSON adapter for {@code type}, creating it if necessary. */ /** Returns a JSON adapter for {@code type}, creating it if necessary. */
@CheckReturnValue public <T> JsonAdapter<T> adapter(Type type) { public <T> JsonAdapter<T> adapter(Type type) {
return adapter(type, Util.NO_ANNOTATIONS); return adapter(type, Util.NO_ANNOTATIONS);
} }
@CheckReturnValue public <T> JsonAdapter<T> adapter(Class<T> type) { public <T> JsonAdapter<T> adapter(Class<T> type) {
return adapter(type, Util.NO_ANNOTATIONS); return adapter(type, Util.NO_ANNOTATIONS);
} }
@CheckReturnValue
public <T> JsonAdapter<T> adapter(Type type, Class<? extends Annotation> annotationType) {
if (annotationType == null) {
throw new NullPointerException("annotationType == null");
}
return adapter(type,
Collections.singleton(Types.createJsonQualifierImplementation(annotationType)));
}
@CheckReturnValue
public <T> JsonAdapter<T> adapter(Type type, Class<? extends Annotation>... annotationTypes) {
if (annotationTypes.length == 1) {
return adapter(type, annotationTypes[0]);
}
Set<Annotation> annotations = new LinkedHashSet<>(annotationTypes.length);
for (Class<? extends Annotation> annotationType : annotationTypes) {
annotations.add(Types.createJsonQualifierImplementation(annotationType));
}
return adapter(type, Collections.unmodifiableSet(annotations));
}
@CheckReturnValue
public <T> JsonAdapter<T> adapter(Type type, Set<? extends Annotation> annotations) {
return adapter(type, annotations, null);
}
/**
* @param fieldName An optional field name associated with this type. The field name is used as a
* hint for better adapter lookup error messages for nested structures.
*/
@CheckReturnValue
@SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters. @SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters.
public <T> JsonAdapter<T> adapter(Type type, Set<? extends Annotation> annotations, public <T> JsonAdapter<T> adapter(Type type, Set<? extends Annotation> annotations) {
@Nullable String fieldName) {
if (type == null) {
throw new NullPointerException("type == null");
}
if (annotations == null) {
throw new NullPointerException("annotations == null");
}
type = removeSubtypeWildcard(canonicalize(type));
// If there's an equivalent adapter in the cache, we're done! // If there's an equivalent adapter in the cache, we're done!
Object cacheKey = cacheKey(type, annotations); Object cacheKey = cacheKey(type, annotations);
synchronized (adapterCache) { synchronized (adapterCache) {
@ -122,45 +63,47 @@ public final class Moshi {
if (result != null) return (JsonAdapter<T>) result; if (result != null) return (JsonAdapter<T>) result;
} }
LookupChain lookupChain = lookupChainThreadLocal.get(); // Short-circuit if this is a reentrant call.
if (lookupChain == null) { List<DeferredAdapter<?>> deferredAdapters = reentrantCalls.get();
lookupChain = new LookupChain(); if (deferredAdapters != null) {
lookupChainThreadLocal.set(lookupChain); for (int i = 0, size = deferredAdapters.size(); i < size; i++) {
DeferredAdapter<?> deferredAdapter = deferredAdapters.get(i);
if (deferredAdapter.cacheKey.equals(cacheKey)) {
return (JsonAdapter<T>) deferredAdapter;
}
}
} else {
deferredAdapters = new ArrayList<>();
reentrantCalls.set(deferredAdapters);
} }
boolean success = false; // Prepare for re-entrant calls, then ask each factory to create a type adapter.
JsonAdapter<T> adapterFromCall = lookupChain.push(type, fieldName, cacheKey); DeferredAdapter<T> deferredAdapter = new DeferredAdapter<>(cacheKey);
deferredAdapters.add(deferredAdapter);
try { try {
if (adapterFromCall != null) return adapterFromCall;
// Ask each factory to create the JSON adapter.
for (int i = 0, size = factories.size(); i < size; i++) { for (int i = 0, size = factories.size(); i < size; i++) {
JsonAdapter<T> result = (JsonAdapter<T>) factories.get(i).create(type, annotations, this); JsonAdapter<T> result = (JsonAdapter<T>) factories.get(i).create(type, annotations, this);
if (result == null) continue; if (result != null) {
deferredAdapter.ready(result);
// Success! Notify the LookupChain so it is cached and can be used by re-entrant calls. synchronized (adapterCache) {
lookupChain.adapterFound(result); adapterCache.put(cacheKey, result);
success = true; }
return result; return result;
}
} }
throw new IllegalArgumentException(
"No JsonAdapter for " + typeAnnotatedWithAnnotations(type, annotations));
} catch (IllegalArgumentException e) {
throw lookupChain.exceptionWithLookupStack(e);
} finally { } finally {
lookupChain.pop(success); deferredAdapters.remove(deferredAdapters.size() - 1);
if (deferredAdapters.isEmpty()) {
reentrantCalls.remove();
}
} }
throw new IllegalArgumentException("No JsonAdapter for " + type + " annotated " + annotations);
} }
@CheckReturnValue
@SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters. @SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters.
public <T> JsonAdapter<T> nextAdapter(JsonAdapter.Factory skipPast, Type type, public <T> JsonAdapter<T> nextAdapter(JsonAdapter.Factory skipPast, Type type,
Set<? extends Annotation> annotations) { Set<? extends Annotation> annotations) {
if (annotations == null) throw new NullPointerException("annotations == null");
type = removeSubtypeWildcard(canonicalize(type));
int skipPastIndex = factories.indexOf(skipPast); int skipPastIndex = factories.indexOf(skipPast);
if (skipPastIndex == -1) { if (skipPastIndex == -1) {
throw new IllegalArgumentException("Unable to skip past unknown factory " + skipPast); throw new IllegalArgumentException("Unable to skip past unknown factory " + skipPast);
@ -170,15 +113,7 @@ public final class Moshi {
if (result != null) return result; if (result != null) return result;
} }
throw new IllegalArgumentException("No next JsonAdapter for " throw new IllegalArgumentException("No next JsonAdapter for "
+ typeAnnotatedWithAnnotations(type, annotations)); + type + " annotated " + annotations);
}
/** Returns a new builder containing all custom factories used by the current instance. */
@CheckReturnValue public Moshi.Builder newBuilder() {
int fullSize = factories.size();
int tailSize = BUILT_IN_FACTORIES.size();
List<JsonAdapter.Factory> customFactories = factories.subList(0, fullSize - tailSize);
return new Builder().addAll(customFactories);
} }
/** Returns an opaque object that's equal if the type and annotations are equal. */ /** Returns an opaque object that's equal if the type and annotations are equal. */
@ -188,14 +123,14 @@ public final class Moshi {
} }
public static final class Builder { public static final class Builder {
final List<JsonAdapter.Factory> factories = new ArrayList<>(); private final List<JsonAdapter.Factory> factories = new ArrayList<>();
public <T> Builder add(final Type type, final JsonAdapter<T> jsonAdapter) { public <T> Builder add(final Type type, final JsonAdapter<T> jsonAdapter) {
if (type == null) throw new IllegalArgumentException("type == null"); if (type == null) throw new IllegalArgumentException("type == null");
if (jsonAdapter == null) throw new IllegalArgumentException("jsonAdapter == null"); if (jsonAdapter == null) throw new IllegalArgumentException("jsonAdapter == null");
return add(new JsonAdapter.Factory() { return add(new JsonAdapter.Factory() {
@Override public @Nullable JsonAdapter<?> create( @Override public JsonAdapter<?> create(
Type targetType, Set<? extends Annotation> annotations, Moshi moshi) { Type targetType, Set<? extends Annotation> annotations, Moshi moshi) {
return annotations.isEmpty() && Util.typesMatch(type, targetType) ? jsonAdapter : null; return annotations.isEmpty() && Util.typesMatch(type, targetType) ? jsonAdapter : null;
} }
@ -215,7 +150,7 @@ public final class Moshi {
} }
return add(new JsonAdapter.Factory() { return add(new JsonAdapter.Factory() {
@Override public @Nullable JsonAdapter<?> create( @Override public JsonAdapter<?> create(
Type targetType, Set<? extends Annotation> annotations, Moshi moshi) { Type targetType, Set<? extends Annotation> annotations, Moshi moshi) {
if (Util.typesMatch(type, targetType) if (Util.typesMatch(type, targetType)
&& annotations.size() == 1 && annotations.size() == 1
@ -227,154 +162,49 @@ public final class Moshi {
}); });
} }
public Builder add(JsonAdapter.Factory factory) { public Builder add(JsonAdapter.Factory jsonAdapter) {
if (factory == null) throw new IllegalArgumentException("factory == null"); factories.add(jsonAdapter);
factories.add(factory);
return this; return this;
} }
public Builder add(Object adapter) { public Builder add(Object adapter) {
if (adapter == null) throw new IllegalArgumentException("adapter == null");
return add(AdapterMethodsFactory.get(adapter)); return add(AdapterMethodsFactory.get(adapter));
} }
Builder addAll(List<JsonAdapter.Factory> factories) { public Moshi build() {
this.factories.addAll(factories);
return this;
}
@CheckReturnValue public Moshi build() {
return new Moshi(this); return new Moshi(this);
} }
} }
/** /**
* A possibly-reentrant chain of lookups for JSON adapters. * Sometimes a type adapter factory depends on its own product; either directly or indirectly.
* To make this work, we offer this type adapter stub while the final adapter is being computed.
* When it is ready, we wire this to delegate to that finished adapter.
* *
* <p>We keep track of the current stack of lookups: we may start by looking up the JSON adapter * <p>Typically this is necessary in self-referential object models, such as an {@code Employee}
* for Employee, re-enter looking for the JSON adapter of HomeAddress, and re-enter again looking * class that has a {@code List<Employee>} field for an organization's management hierarchy.
* up the JSON adapter of PostalCode. If any of these lookups fail we can provide a stack trace
* with all of the lookups.
*
* <p>Sometimes a JSON adapter factory depends on its own product; either directly or indirectly.
* To make this work, we offer a JSON adapter stub while the final adapter is being computed.
* When it is ready, we wire the stub to that finished adapter. This is necessary in
* self-referential object models, such as an {@code Employee} class that has a {@code
* List<Employee>} field for an organization's management hierarchy.
*
* <p>This class defers putting any JSON adapters in the cache until the topmost JSON adapter has
* successfully been computed. That way we don't pollute the cache with incomplete stubs, or
* adapters that may transitively depend on incomplete stubs.
*/ */
final class LookupChain { private static class DeferredAdapter<T> extends JsonAdapter<T> {
final List<Lookup<?>> callLookups = new ArrayList<>(); private Object cacheKey;
final Deque<Lookup<?>> stack = new ArrayDeque<>(); private JsonAdapter<T> delegate;
boolean exceptionAnnotated;
/** public DeferredAdapter(Object cacheKey) {
* Returns a JSON adapter that was already created for this call, or null if this is the first
* time in this call that the cache key has been requested in this call. This may return a
* lookup that isn't yet ready if this lookup is reentrant.
*/
<T> JsonAdapter<T> push(Type type, @Nullable String fieldName, Object cacheKey) {
// Try to find a lookup with the same key for the same call.
for (int i = 0, size = callLookups.size(); i < size; i++) {
Lookup<?> lookup = callLookups.get(i);
if (lookup.cacheKey.equals(cacheKey)) {
Lookup<T> hit = (Lookup<T>) lookup;
stack.add(hit);
return hit.adapter != null ? hit.adapter : hit;
}
}
// We might need to know about this cache key later in this call. Prepare for that.
Lookup<Object> lookup = new Lookup<>(type, fieldName, cacheKey);
callLookups.add(lookup);
stack.add(lookup);
return null;
}
/** Sets the adapter result of the current lookup. */
<T> void adapterFound(JsonAdapter<T> result) {
Lookup<T> currentLookup = (Lookup<T>) stack.getLast();
currentLookup.adapter = result;
}
/**
* Completes the current lookup by removing a stack frame.
*
* @param success true if the adapter cache should be populated if this is the topmost lookup.
*/
void pop(boolean success) {
stack.removeLast();
if (!stack.isEmpty()) return;
lookupChainThreadLocal.remove();
if (success) {
synchronized (adapterCache) {
for (int i = 0, size = callLookups.size(); i < size; i++) {
Lookup<?> lookup = callLookups.get(i);
JsonAdapter<?> replaced = adapterCache.put(lookup.cacheKey, lookup.adapter);
if (replaced != null) {
((Lookup<Object>) lookup).adapter = (JsonAdapter<Object>) replaced;
adapterCache.put(lookup.cacheKey, replaced);
}
}
}
}
}
IllegalArgumentException exceptionWithLookupStack(IllegalArgumentException e) {
// Don't add the lookup stack to more than one exception; the deepest is sufficient.
if (exceptionAnnotated) return e;
exceptionAnnotated = true;
int size = stack.size();
if (size == 1 && stack.getFirst().fieldName == null) return e;
StringBuilder errorMessageBuilder = new StringBuilder(e.getMessage());
for (Iterator<Lookup<?>> i = stack.descendingIterator(); i.hasNext(); ) {
Lookup<?> lookup = i.next();
errorMessageBuilder
.append("\nfor ")
.append(lookup.type);
if (lookup.fieldName != null) {
errorMessageBuilder
.append(' ')
.append(lookup.fieldName);
}
}
return new IllegalArgumentException(errorMessageBuilder.toString(), e);
}
}
/** This class implements {@code JsonAdapter} so it can be used as a stub for re-entrant calls. */
static final class Lookup<T> extends JsonAdapter<T> {
final Type type;
final @Nullable String fieldName;
final Object cacheKey;
@Nullable JsonAdapter<T> adapter;
Lookup(Type type, @Nullable String fieldName, Object cacheKey) {
this.type = type;
this.fieldName = fieldName;
this.cacheKey = cacheKey; this.cacheKey = cacheKey;
} }
public void ready(JsonAdapter<T> delegate) {
this.delegate = delegate;
this.cacheKey = null;
}
@Override public T fromJson(JsonReader reader) throws IOException { @Override public T fromJson(JsonReader reader) throws IOException {
if (adapter == null) throw new IllegalStateException("JsonAdapter isn't ready"); if (delegate == null) throw new IllegalStateException("Type adapter isn't ready");
return adapter.fromJson(reader); return delegate.fromJson(reader);
} }
@Override public void toJson(JsonWriter writer, T value) throws IOException { @Override public void toJson(JsonWriter writer, T value) throws IOException {
if (adapter == null) throw new IllegalStateException("JsonAdapter isn't ready"); if (delegate == null) throw new IllegalStateException("Type adapter isn't ready");
adapter.toJson(writer, value); delegate.toJson(writer, value);
}
@Override public String toString() {
return adapter != null ? adapter.toString() : super.toString();
} }
} }
} }

View file

@ -15,23 +15,17 @@
*/ */
package com.squareup.moshi; package com.squareup.moshi;
import com.squareup.moshi.internal.Util;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import javax.annotation.Nullable;
import static com.squareup.moshi.internal.Util.generatedAdapter;
final class StandardJsonAdapters { final class StandardJsonAdapters {
private StandardJsonAdapters() {
}
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() { public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
@Override public JsonAdapter<?> create( @Override public JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) { Type type, Set<? extends Annotation> annotations, Moshi moshi) {
@ -56,15 +50,9 @@ final class StandardJsonAdapters {
if (type == Object.class) return new ObjectJsonAdapter(moshi).nullSafe(); if (type == Object.class) return new ObjectJsonAdapter(moshi).nullSafe();
Class<?> rawType = Types.getRawType(type); Class<?> rawType = Types.getRawType(type);
@Nullable JsonAdapter<?> generatedAdapter = generatedAdapter(moshi, type, rawType);
if (generatedAdapter != null) {
return generatedAdapter;
}
if (rawType.isEnum()) { if (rawType.isEnum()) {
//noinspection unchecked //noinspection unchecked
return new EnumJsonAdapter<>((Class<? extends Enum>) rawType).nullSafe(); return enumAdapter((Class<? extends Enum>) rawType).nullSafe();
} }
return null; return null;
} }
@ -72,7 +60,7 @@ final class StandardJsonAdapters {
private static final String ERROR_FORMAT = "Expected %s but was %s at path %s"; private static final String ERROR_FORMAT = "Expected %s but was %s at path %s";
static int rangeCheckNextInt(JsonReader reader, String typeMessage, int min, int max) private static int rangeCheckNextInt(JsonReader reader, String typeMessage, int min, int max)
throws IOException { throws IOException {
int value = reader.nextInt(); int value = reader.nextInt();
if (value < min || value > max) { if (value < min || value > max) {
@ -88,7 +76,7 @@ final class StandardJsonAdapters {
} }
@Override public void toJson(JsonWriter writer, Boolean value) throws IOException { @Override public void toJson(JsonWriter writer, Boolean value) throws IOException {
writer.value(value.booleanValue()); writer.value(value);
} }
@Override public String toString() { @Override public String toString() {
@ -224,47 +212,27 @@ final class StandardJsonAdapters {
} }
}; };
static final class EnumJsonAdapter<T extends Enum<T>> extends JsonAdapter<T> { static <T extends Enum<T>> JsonAdapter<T> enumAdapter(final Class<T> enumType) {
private final Class<T> enumType; return new JsonAdapter<T>() {
private final String[] nameStrings; @Override public T fromJson(JsonReader reader) throws IOException {
private final T[] constants; String name = reader.nextString();
private final JsonReader.Options options; try {
return Enum.valueOf(enumType, name);
EnumJsonAdapter(Class<T> enumType) { } catch (IllegalArgumentException e) {
this.enumType = enumType; throw new JsonDataException("Expected one of "
try { + Arrays.toString(enumType.getEnumConstants()) + " but was " + name + " at path "
constants = enumType.getEnumConstants(); + reader.getPath());
nameStrings = new String[constants.length];
for (int i = 0; i < constants.length; i++) {
T constant = constants[i];
Json annotation = enumType.getField(constant.name()).getAnnotation(Json.class);
String name = annotation != null ? annotation.name() : constant.name();
nameStrings[i] = name;
} }
options = JsonReader.Options.of(nameStrings);
} catch (NoSuchFieldException e) {
throw new AssertionError("Missing field in " + enumType.getName(), e);
} }
}
@Override public T fromJson(JsonReader reader) throws IOException { @Override public void toJson(JsonWriter writer, T value) throws IOException {
int index = reader.selectString(options); writer.value(value.name());
if (index != -1) return constants[index]; }
// We can consume the string safely, we are terminating anyway. @Override public String toString() {
String path = reader.getPath(); return "JsonAdapter(" + enumType.getName() + ")";
String name = reader.nextString(); }
throw new JsonDataException("Expected one of " };
+ Arrays.asList(nameStrings) + " but was " + name + " at path " + path);
}
@Override public void toJson(JsonWriter writer, T value) throws IOException {
writer.value(nameStrings[value.ordinal()]);
}
@Override public String toString() {
return "JsonAdapter(" + enumType.getName() + ")";
}
} }
/** /**
@ -277,44 +245,46 @@ final class StandardJsonAdapters {
*/ */
static final class ObjectJsonAdapter extends JsonAdapter<Object> { static final class ObjectJsonAdapter extends JsonAdapter<Object> {
private final Moshi moshi; private final Moshi moshi;
private final JsonAdapter<List> listJsonAdapter;
private final JsonAdapter<Map> mapAdapter;
private final JsonAdapter<String> stringAdapter;
private final JsonAdapter<Double> doubleAdapter;
private final JsonAdapter<Boolean> booleanAdapter;
ObjectJsonAdapter(Moshi moshi) { public ObjectJsonAdapter(Moshi moshi) {
this.moshi = moshi; this.moshi = moshi;
this.listJsonAdapter = moshi.adapter(List.class);
this.mapAdapter = moshi.adapter(Map.class);
this.stringAdapter = moshi.adapter(String.class);
this.doubleAdapter = moshi.adapter(Double.class);
this.booleanAdapter = moshi.adapter(Boolean.class);
} }
@Override public Object fromJson(JsonReader reader) throws IOException { @Override public Object fromJson(JsonReader reader) throws IOException {
switch (reader.peek()) { switch (reader.peek()) {
case BEGIN_ARRAY: case BEGIN_ARRAY:
return listJsonAdapter.fromJson(reader); List<Object> list = new ArrayList<>();
reader.beginArray();
while (reader.hasNext()) {
list.add(fromJson(reader));
}
reader.endArray();
return list;
case BEGIN_OBJECT: case BEGIN_OBJECT:
return mapAdapter.fromJson(reader); Map<String, Object> map = new LinkedHashTreeMap<>();
reader.beginObject();
while (reader.hasNext()) {
map.put(reader.nextName(), fromJson(reader));
}
reader.endObject();
return map;
case STRING: case STRING:
return stringAdapter.fromJson(reader); return reader.nextString();
case NUMBER: case NUMBER:
return doubleAdapter.fromJson(reader); return reader.nextDouble();
case BOOLEAN: case BOOLEAN:
return booleanAdapter.fromJson(reader); return reader.nextBoolean();
case NULL: case NULL:
return reader.nextNull(); return reader.nextNull();
default: default:
throw new IllegalStateException( throw new IllegalStateException("Expected a value but was " + reader.peek()
"Expected a value but was " + reader.peek() + " at path " + reader.getPath()); + " at path " + reader.getPath());
} }
} }

View file

@ -15,112 +15,34 @@
*/ */
package com.squareup.moshi; package com.squareup.moshi;
import com.squareup.moshi.internal.Util.GenericArrayTypeImpl;
import com.squareup.moshi.internal.Util.ParameterizedTypeImpl;
import com.squareup.moshi.internal.Util.WildcardTypeImpl;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array; import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType; import java.lang.reflect.GenericArrayType;
import java.lang.reflect.InvocationHandler; import java.lang.reflect.GenericDeclaration;
import java.lang.reflect.Method; import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType; import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable; import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType; import java.lang.reflect.WildcardType;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map; import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties; import java.util.Properties;
import java.util.Set;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import static com.squareup.moshi.internal.Util.EMPTY_TYPE_ARRAY;
import static com.squareup.moshi.internal.Util.getGenericSupertype;
import static com.squareup.moshi.internal.Util.resolve;
/** Factory methods for types. */ /** Factory methods for types. */
@CheckReturnValue
public final class Types { public final class Types {
static final Type[] EMPTY_TYPE_ARRAY = new Type[] {};
private Types() { private Types() {
} }
/** /**
* Resolves the generated {@link JsonAdapter} fully qualified class name for a given * Returns a new parameterized type, applying {@code typeArguments} to {@code rawType}.
* {@link JsonClass JsonClass-annotated} {@code clazz}. This is the same lookup logic used by
* both the Moshi code generation as well as lookup for any JsonClass-annotated classes. This can
* be useful if generating your own JsonAdapters without using Moshi's first party code gen.
*
* @param clazz the class to calculate a generated JsonAdapter name for.
* @return the resolved fully qualified class name to the expected generated JsonAdapter class.
* Note that this name will always be a top-level class name and not a nested class.
*/
public static String generatedJsonAdapterName(Class<?> clazz) {
if (clazz.getAnnotation(JsonClass.class) == null) {
throw new IllegalArgumentException("Class does not have a JsonClass annotation: " + clazz);
}
return generatedJsonAdapterName(clazz.getName());
}
/**
* Resolves the generated {@link JsonAdapter} fully qualified class name for a given
* {@link JsonClass JsonClass-annotated} {@code className}. This is the same lookup logic used by
* both the Moshi code generation as well as lookup for any JsonClass-annotated classes. This can
* be useful if generating your own JsonAdapters without using Moshi's first party code gen.
*
* @param className the fully qualified class to calculate a generated JsonAdapter name for.
* @return the resolved fully qualified class name to the expected generated JsonAdapter class.
* Note that this name will always be a top-level class name and not a nested class.
*/
public static String generatedJsonAdapterName(String className) {
return className.replace("$", "_") + "JsonAdapter";
}
/**
* Checks if {@code annotations} contains {@code jsonQualifier}.
* Returns the subset of {@code annotations} without {@code jsonQualifier}, or null if {@code
* annotations} does not contain {@code jsonQualifier}.
*/
public static @Nullable Set<? extends Annotation> nextAnnotations(
Set<? extends Annotation> annotations,
Class<? extends Annotation> jsonQualifier) {
if (!jsonQualifier.isAnnotationPresent(JsonQualifier.class)) {
throw new IllegalArgumentException(jsonQualifier + " is not a JsonQualifier.");
}
if (annotations.isEmpty()) {
return null;
}
for (Annotation annotation : annotations) {
if (jsonQualifier.equals(annotation.annotationType())) {
Set<? extends Annotation> delegateAnnotations = new LinkedHashSet<>(annotations);
delegateAnnotations.remove(annotation);
return Collections.unmodifiableSet(delegateAnnotations);
}
}
return null;
}
/**
* Returns a new parameterized type, applying {@code typeArguments} to {@code rawType}. Use this
* method if {@code rawType} is not enclosed in another type.
*/ */
public static ParameterizedType newParameterizedType(Type rawType, Type... typeArguments) { public static ParameterizedType newParameterizedType(Type rawType, Type... typeArguments) {
return new ParameterizedTypeImpl(null, rawType, typeArguments); return new ParameterizedTypeImpl(null, rawType, typeArguments);
} }
/**
* Returns a new parameterized type, applying {@code typeArguments} to {@code rawType}. Use this
* method if {@code rawType} is enclosed in {@code ownerType}.
*/
public static ParameterizedType newParameterizedTypeWithOwner(
Type ownerType, Type rawType, Type... typeArguments) {
return new ParameterizedTypeImpl(ownerType, rawType, typeArguments);
}
/** Returns an array type whose elements are all instances of {@code componentType}. */ /** Returns an array type whose elements are all instances of {@code componentType}. */
public static GenericArrayType arrayOf(Type componentType) { public static GenericArrayType arrayOf(Type componentType) {
return new GenericArrayTypeImpl(componentType); return new GenericArrayTypeImpl(componentType);
@ -144,6 +66,33 @@ public final class Types {
return new WildcardTypeImpl(new Type[] { Object.class }, new Type[] { bound }); return new WildcardTypeImpl(new Type[] { Object.class }, new Type[] { bound });
} }
/**
* Returns a type that is functionally equal but not necessarily equal according to {@link
* Object#equals(Object) Object.equals()}.
*/
static Type canonicalize(Type type) {
if (type instanceof Class) {
Class<?> c = (Class<?>) type;
return c.isArray() ? new GenericArrayTypeImpl(canonicalize(c.getComponentType())) : c;
} else if (type instanceof ParameterizedType) {
ParameterizedType p = (ParameterizedType) type;
return new ParameterizedTypeImpl(p.getOwnerType(),
p.getRawType(), p.getActualTypeArguments());
} else if (type instanceof GenericArrayType) {
GenericArrayType g = (GenericArrayType) type;
return new GenericArrayTypeImpl(g.getGenericComponentType());
} else if (type instanceof WildcardType) {
WildcardType w = (WildcardType) type;
return new WildcardTypeImpl(w.getUpperBounds(), w.getLowerBounds());
} else {
return type; // This type is unsupported!
}
}
public static Class<?> getRawType(Type type) { public static Class<?> getRawType(Type type) {
if (type instanceof Class<?>) { if (type instanceof Class<?>) {
// type is a normal class. // type is a normal class.
@ -158,7 +107,7 @@ public final class Types {
return (Class<?>) rawType; return (Class<?>) rawType;
} else if (type instanceof GenericArrayType) { } else if (type instanceof GenericArrayType) {
Type componentType = ((GenericArrayType) type).getGenericComponentType(); Type componentType = ((GenericArrayType)type).getGenericComponentType();
return Array.newInstance(getRawType(componentType), 0).getClass(); return Array.newInstance(getRawType(componentType), 0).getClass();
} else if (type instanceof TypeVariable) { } else if (type instanceof TypeVariable) {
@ -176,32 +125,16 @@ public final class Types {
} }
} }
/** static boolean equal(Object a, Object b) {
* Returns the element type of this collection type. return a == b || (a != null && a.equals(b));
* @throws IllegalArgumentException if this type is not a collection.
*/
public static Type collectionElementType(Type context, Class<?> contextRawType) {
Type collectionType = getSupertype(context, contextRawType, Collection.class);
if (collectionType instanceof WildcardType) {
collectionType = ((WildcardType) collectionType).getUpperBounds()[0];
}
if (collectionType instanceof ParameterizedType) {
return ((ParameterizedType) collectionType).getActualTypeArguments()[0];
}
return Object.class;
} }
/** Returns true if {@code a} and {@code b} are equal. */ /** Returns true if {@code a} and {@code b} are equal. */
public static boolean equals(@Nullable Type a, @Nullable Type b) { static boolean equals(Type a, Type b) {
if (a == b) { if (a == b) {
return true; // Also handles (a == null && b == null). return true; // Also handles (a == null && b == null).
} else if (a instanceof Class) { } else if (a instanceof Class) {
if (b instanceof GenericArrayType) {
return equals(((Class) a).getComponentType(),
((GenericArrayType) b).getGenericComponentType());
}
return a.equals(b); // Class already specifies equals(). return a.equals(b); // Class already specifies equals().
} else if (a instanceof ParameterizedType) { } else if (a instanceof ParameterizedType) {
@ -214,15 +147,11 @@ public final class Types {
Type[] bTypeArguments = pb instanceof ParameterizedTypeImpl Type[] bTypeArguments = pb instanceof ParameterizedTypeImpl
? ((ParameterizedTypeImpl) pb).typeArguments ? ((ParameterizedTypeImpl) pb).typeArguments
: pb.getActualTypeArguments(); : pb.getActualTypeArguments();
return equals(pa.getOwnerType(), pb.getOwnerType()) return equal(pa.getOwnerType(), pb.getOwnerType())
&& pa.getRawType().equals(pb.getRawType()) && pa.getRawType().equals(pb.getRawType())
&& Arrays.equals(aTypeArguments, bTypeArguments); && Arrays.equals(aTypeArguments, bTypeArguments);
} else if (a instanceof GenericArrayType) { } else if (a instanceof GenericArrayType) {
if (b instanceof Class) {
return equals(((Class) b).getComponentType(),
((GenericArrayType) a).getGenericComponentType());
}
if (!(b instanceof GenericArrayType)) return false; if (!(b instanceof GenericArrayType)) return false;
GenericArrayType ga = (GenericArrayType) a; GenericArrayType ga = (GenericArrayType) a;
GenericArrayType gb = (GenericArrayType) b; GenericArrayType gb = (GenericArrayType) b;
@ -243,87 +172,56 @@ public final class Types {
&& va.getName().equals(vb.getName()); && va.getName().equals(vb.getName());
} else { } else {
// This isn't a supported type. // This isn't a supported type. Could be a generic array type, wildcard type, etc.
return false; return false;
} }
} }
private static int hashCodeOrZero(Object o) {
return o != null ? o.hashCode() : 0;
}
static String typeToString(Type type) {
return type instanceof Class ? ((Class<?>) type).getName() : type.toString();
}
/** /**
* @param clazz the target class to read the {@code fieldName} field annotations from. * Returns the generic supertype for {@code supertype}. For example, given a class {@code
* @param fieldName the target field name on {@code clazz}. * IntegerSet}, the result for when supertype is {@code Set.class} is {@code Set<Integer>} and the
* @return a set of {@link JsonQualifier}-annotated {@link Annotation} instances retrieved from * result when the supertype is {@code Collection.class} is {@code Collection<Integer>}.
* the targeted field. Can be empty if none are found.
*/ */
public static Set<? extends Annotation> getFieldJsonQualifierAnnotations(Class<?> clazz, static Type getGenericSupertype(Type context, Class<?> rawType, Class<?> toResolve) {
String fieldName) { if (toResolve == rawType) {
try { return context;
Field field = clazz.getDeclaredField(fieldName); }
field.setAccessible(true);
Annotation[] fieldAnnotations = field.getDeclaredAnnotations(); // we skip searching through interfaces if unknown is an interface
Set<Annotation> annotations = new LinkedHashSet<>(fieldAnnotations.length); if (toResolve.isInterface()) {
for (Annotation annotation : fieldAnnotations) { Class<?>[] interfaces = rawType.getInterfaces();
if (annotation.annotationType().isAnnotationPresent(JsonQualifier.class)) { for (int i = 0, length = interfaces.length; i < length; i++) {
annotations.add(annotation); if (interfaces[i] == toResolve) {
return rawType.getGenericInterfaces()[i];
} else if (toResolve.isAssignableFrom(interfaces[i])) {
return getGenericSupertype(rawType.getGenericInterfaces()[i], interfaces[i], toResolve);
} }
} }
return Collections.unmodifiableSet(annotations);
} catch (NoSuchFieldException e) {
throw new IllegalArgumentException("Could not access field "
+ fieldName
+ " on class "
+ clazz.getCanonicalName(),
e);
} }
}
@SuppressWarnings("unchecked") // check our supertypes
static <T extends Annotation> T createJsonQualifierImplementation(final Class<T> annotationType) { if (!rawType.isInterface()) {
if (!annotationType.isAnnotation()) { while (rawType != Object.class) {
throw new IllegalArgumentException(annotationType + " must be an annotation."); Class<?> rawSupertype = rawType.getSuperclass();
if (rawSupertype == toResolve) {
return rawType.getGenericSuperclass();
} else if (toResolve.isAssignableFrom(rawSupertype)) {
return getGenericSupertype(rawType.getGenericSuperclass(), rawSupertype, toResolve);
}
rawType = rawSupertype;
}
} }
if (!annotationType.isAnnotationPresent(JsonQualifier.class)) {
throw new IllegalArgumentException(annotationType + " must have @JsonQualifier.");
}
if (annotationType.getDeclaredMethods().length != 0) {
throw new IllegalArgumentException(annotationType + " must not declare methods.");
}
return (T) Proxy.newProxyInstance(annotationType.getClassLoader(),
new Class<?>[] { annotationType }, new InvocationHandler() {
@Override public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
String methodName = method.getName();
switch (methodName) {
case "annotationType":
return annotationType;
case "equals":
Object o = args[0];
return annotationType.isInstance(o);
case "hashCode":
return 0;
case "toString":
return "@" + annotationType.getName() + "()";
default:
return method.invoke(proxy, args);
}
}
});
}
/** // we can't resolve this further
* Returns a two element array containing this map's key and value types in positions 0 and 1 return toResolve;
* respectively.
*/
static Type[] mapKeyAndValueTypes(Type context, Class<?> contextRawType) {
// Work around a problem with the declaration of java.util.Properties. That class should extend
// Hashtable<String, String>, but it's declared to extend Hashtable<Object, Object>.
if (context == Properties.class) return new Type[] { String.class, String.class };
Type mapType = getSupertype(context, contextRawType, Map.class);
if (mapType instanceof ParameterizedType) {
ParameterizedType mapParameterizedType = (ParameterizedType) mapType;
return mapParameterizedType.getActualTypeArguments();
}
return new Type[] { Object.class, Object.class };
} }
/** /**
@ -357,4 +255,290 @@ public final class Types {
return null; return null;
} }
} }
/**
* Returns the element type of this collection type.
* @throws IllegalArgumentException if this type is not a collection.
*/
public static Type collectionElementType(Type context, Class<?> contextRawType) {
Type collectionType = getSupertype(context, contextRawType, Collection.class);
if (collectionType instanceof WildcardType) {
collectionType = ((WildcardType) collectionType).getUpperBounds()[0];
}
if (collectionType instanceof ParameterizedType) {
return ((ParameterizedType) collectionType).getActualTypeArguments()[0];
}
return Object.class;
}
/**
* Returns a two element array containing this map's key and value types in positions 0 and 1
* respectively.
*/
static Type[] mapKeyAndValueTypes(Type context, Class<?> contextRawType) {
// Work around a problem with the declaration of java.util.Properties. That class should extend
// Hashtable<String, String>, but it's declared to extend Hashtable<Object, Object>.
if (context == Properties.class) return new Type[] { String.class, String.class };
Type mapType = getSupertype(context, contextRawType, Map.class);
if (mapType instanceof ParameterizedType) {
ParameterizedType mapParameterizedType = (ParameterizedType) mapType;
return mapParameterizedType.getActualTypeArguments();
}
return new Type[] { Object.class, Object.class };
}
static Type resolve(Type context, Class<?> contextRawType, Type toResolve) {
// This implementation is made a little more complicated in an attempt to avoid object-creation.
while (true) {
if (toResolve instanceof TypeVariable) {
TypeVariable<?> typeVariable = (TypeVariable<?>) toResolve;
toResolve = resolveTypeVariable(context, contextRawType, typeVariable);
if (toResolve == typeVariable) return toResolve;
} else if (toResolve instanceof Class && ((Class<?>) toResolve).isArray()) {
Class<?> original = (Class<?>) toResolve;
Type componentType = original.getComponentType();
Type newComponentType = resolve(context, contextRawType, componentType);
return componentType == newComponentType
? original
: arrayOf(newComponentType);
} else if (toResolve instanceof GenericArrayType) {
GenericArrayType original = (GenericArrayType) toResolve;
Type componentType = original.getGenericComponentType();
Type newComponentType = resolve(context, contextRawType, componentType);
return componentType == newComponentType
? original
: arrayOf(newComponentType);
} else if (toResolve instanceof ParameterizedType) {
ParameterizedType original = (ParameterizedType) toResolve;
Type ownerType = original.getOwnerType();
Type newOwnerType = resolve(context, contextRawType, ownerType);
boolean changed = newOwnerType != ownerType;
Type[] args = original.getActualTypeArguments();
for (int t = 0, length = args.length; t < length; t++) {
Type resolvedTypeArgument = resolve(context, contextRawType, args[t]);
if (resolvedTypeArgument != args[t]) {
if (!changed) {
args = args.clone();
changed = true;
}
args[t] = resolvedTypeArgument;
}
}
return changed
? new ParameterizedTypeImpl(newOwnerType, original.getRawType(), args)
: original;
} else if (toResolve instanceof WildcardType) {
WildcardType original = (WildcardType) toResolve;
Type[] originalLowerBound = original.getLowerBounds();
Type[] originalUpperBound = original.getUpperBounds();
if (originalLowerBound.length == 1) {
Type lowerBound = resolve(context, contextRawType, originalLowerBound[0]);
if (lowerBound != originalLowerBound[0]) {
return supertypeOf(lowerBound);
}
} else if (originalUpperBound.length == 1) {
Type upperBound = resolve(context, contextRawType, originalUpperBound[0]);
if (upperBound != originalUpperBound[0]) {
return subtypeOf(upperBound);
}
}
return original;
} else {
return toResolve;
}
}
}
static Type resolveTypeVariable(Type context, Class<?> contextRawType, TypeVariable<?> unknown) {
Class<?> declaredByRaw = declaringClassOf(unknown);
// We can't reduce this further.
if (declaredByRaw == null) return unknown;
Type declaredBy = getGenericSupertype(context, contextRawType, declaredByRaw);
if (declaredBy instanceof ParameterizedType) {
int index = indexOf(declaredByRaw.getTypeParameters(), unknown);
return ((ParameterizedType) declaredBy).getActualTypeArguments()[index];
}
return unknown;
}
private static int indexOf(Object[] array, Object toFind) {
for (int i = 0; i < array.length; i++) {
if (toFind.equals(array[i])) return i;
}
throw new NoSuchElementException();
}
/**
* Returns the declaring class of {@code typeVariable}, or {@code null} if it was not declared by
* a class.
*/
private static Class<?> declaringClassOf(TypeVariable<?> typeVariable) {
GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration();
return genericDeclaration instanceof Class ? (Class<?>) genericDeclaration : null;
}
private static void checkNotPrimitive(Type type) {
if ((type instanceof Class<?>) && ((Class<?>) type).isPrimitive()) {
throw new IllegalArgumentException();
}
}
private static final class ParameterizedTypeImpl implements ParameterizedType {
private final Type ownerType;
private final Type rawType;
private final Type[] typeArguments;
public ParameterizedTypeImpl(Type ownerType, Type rawType, Type... typeArguments) {
// require an owner type if the raw type needs it
if (rawType instanceof Class<?>) {
Class<?> rawTypeAsClass = (Class<?>) rawType;
boolean isStaticOrTopLevelClass = Modifier.isStatic(rawTypeAsClass.getModifiers())
|| rawTypeAsClass.getEnclosingClass() == null;
if (ownerType == null && !isStaticOrTopLevelClass) throw new IllegalArgumentException();
}
this.ownerType = ownerType == null ? null : canonicalize(ownerType);
this.rawType = canonicalize(rawType);
this.typeArguments = typeArguments.clone();
for (int t = 0; t < this.typeArguments.length; t++) {
if (this.typeArguments[t] == null) throw new NullPointerException();
checkNotPrimitive(this.typeArguments[t]);
this.typeArguments[t] = canonicalize(this.typeArguments[t]);
}
}
public Type[] getActualTypeArguments() {
return typeArguments.clone();
}
public Type getRawType() {
return rawType;
}
public Type getOwnerType() {
return ownerType;
}
@Override public boolean equals(Object other) {
return other instanceof ParameterizedType
&& Types.equals(this, (ParameterizedType) other);
}
@Override public int hashCode() {
return Arrays.hashCode(typeArguments)
^ rawType.hashCode()
^ hashCodeOrZero(ownerType);
}
@Override public String toString() {
StringBuilder result = new StringBuilder(30 * (typeArguments.length + 1));
result.append(typeToString(rawType));
if (typeArguments.length == 0) {
return result.toString();
}
result.append("<").append(typeToString(typeArguments[0]));
for (int i = 1; i < typeArguments.length; i++) {
result.append(", ").append(typeToString(typeArguments[i]));
}
return result.append(">").toString();
}
}
private static final class GenericArrayTypeImpl implements GenericArrayType {
private final Type componentType;
public GenericArrayTypeImpl(Type componentType) {
this.componentType = canonicalize(componentType);
}
public Type getGenericComponentType() {
return componentType;
}
@Override public boolean equals(Object o) {
return o instanceof GenericArrayType
&& Types.equals(this, (GenericArrayType) o);
}
@Override public int hashCode() {
return componentType.hashCode();
}
@Override public String toString() {
return typeToString(componentType) + "[]";
}
}
/**
* The WildcardType interface supports multiple upper bounds and multiple lower bounds. We only
* support what the Java 6 language needs - at most one bound. If a lower bound is set, the upper
* bound must be Object.class.
*/
private static final class WildcardTypeImpl implements WildcardType {
private final Type upperBound;
private final Type lowerBound;
public WildcardTypeImpl(Type[] upperBounds, Type[] lowerBounds) {
if (lowerBounds.length > 1) throw new IllegalArgumentException();
if (upperBounds.length != 1) throw new IllegalArgumentException();
if (lowerBounds.length == 1) {
if (lowerBounds[0] == null) throw new NullPointerException();
checkNotPrimitive(lowerBounds[0]);
if (upperBounds[0] != Object.class) throw new IllegalArgumentException();
this.lowerBound = canonicalize(lowerBounds[0]);
this.upperBound = Object.class;
} else {
if (upperBounds[0] == null) throw new NullPointerException();
checkNotPrimitive(upperBounds[0]);
this.lowerBound = null;
this.upperBound = canonicalize(upperBounds[0]);
}
}
public Type[] getUpperBounds() {
return new Type[] { upperBound };
}
public Type[] getLowerBounds() {
return lowerBound != null ? new Type[] { lowerBound } : EMPTY_TYPE_ARRAY;
}
@Override public boolean equals(Object other) {
return other instanceof WildcardType
&& Types.equals(this, (WildcardType) other);
}
@Override public int hashCode() {
// This equals Arrays.hashCode(getLowerBounds()) ^ Arrays.hashCode(getUpperBounds()).
return (lowerBound != null ? 31 + lowerBound.hashCode() : 1)
^ (31 + upperBound.hashCode());
}
@Override public String toString() {
if (lowerBound != null) {
return "? super " + typeToString(lowerBound);
} else if (upperBound == Object.class) {
return "?";
} else {
return "? extends " + typeToString(upperBound);
}
}
}
} }

View 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;
}
}

View file

@ -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()";
}
}

View file

@ -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);
}
}
}

View file

@ -1,3 +0,0 @@
/** Moshi is modern JSON library for Android and Java. */
@javax.annotation.ParametersAreNonnullByDefault
package com.squareup.moshi;

View file

@ -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>;
}

View file

@ -15,24 +15,14 @@
*/ */
package com.squareup.moshi; package com.squareup.moshi;
import com.squareup.moshi.MoshiTest.Uppercase;
import com.squareup.moshi.MoshiTest.UppercaseAdapterFactory;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import okio.ByteString;
import org.junit.Test; import org.junit.Test;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
@ -83,91 +73,6 @@ public final class AdapterMethodsTest {
} }
} }
private static final class PointJsonAdapterWithDelegate {
@FromJson Point fromJson(JsonReader reader, JsonAdapter<Point> delegate) throws IOException {
reader.beginArray();
Point value = delegate.fromJson(reader);
reader.endArray();
return value;
}
@ToJson void toJson(JsonWriter writer, Point value, JsonAdapter<Point> delegate)
throws IOException {
writer.beginArray();
delegate.toJson(writer, value);
writer.endArray();
}
}
private static final class PointJsonAdapterWithDelegateWithQualifier {
@FromJson @WithParens Point fromJson(JsonReader reader, @WithParens JsonAdapter<Point> delegate)
throws IOException {
reader.beginArray();
Point value = delegate.fromJson(reader);
reader.endArray();
return value;
}
@ToJson void toJson(JsonWriter writer, @WithParens Point value,
@WithParens JsonAdapter<Point> delegate)
throws IOException {
writer.beginArray();
delegate.toJson(writer, value);
writer.endArray();
}
}
@Test public void toAndFromWithDelegate() throws Exception {
Moshi moshi = new Moshi.Builder()
.add(new PointJsonAdapterWithDelegate())
.build();
JsonAdapter<Point> adapter = moshi.adapter(Point.class);
Point point = new Point(5, 8);
assertThat(adapter.toJson(point)).isEqualTo("[{\"x\":5,\"y\":8}]");
assertThat(adapter.fromJson("[{\"x\":5,\"y\":8}]")).isEqualTo(point);
}
@Test public void toAndFromWithDelegateWithQualifier() throws Exception {
Moshi moshi = new Moshi.Builder()
.add(new PointJsonAdapterWithDelegateWithQualifier())
.add(new PointWithParensJsonAdapter())
.build();
JsonAdapter<Point> adapter = moshi.adapter(Point.class, WithParens.class);
Point point = new Point(5, 8);
assertThat(adapter.toJson(point)).isEqualTo("[\"(5 8)\"]");
assertThat(adapter.fromJson("[\"(5 8)\"]")).isEqualTo(point);
}
@Test public void toAndFromWithIntermediate() throws Exception {
Moshi moshi = new Moshi.Builder().add(new Object() {
@FromJson String fromJson(String string) {
return string.substring(1, string.length() - 1);
}
@ToJson String toJson(String value) {
return "|" + value + "|";
}
}).build();
JsonAdapter<String> adapter = moshi.adapter(String.class);
assertThat(adapter.toJson("pizza")).isEqualTo("\"|pizza|\"");
assertThat(adapter.fromJson("\"|pizza|\"")).isEqualTo("pizza");
}
@Test public void toAndFromWithIntermediateWithQualifier() throws Exception {
Moshi moshi = new Moshi.Builder().add(new Object() {
@FromJson @Uppercase String fromJson(@Uppercase String string) {
return string.substring(1, string.length() - 1);
}
@ToJson @Uppercase String toJson(@Uppercase String value) {
return "|" + value + "|";
}
}).add(new UppercaseAdapterFactory()).build();
JsonAdapter<String> adapter = moshi.adapter(String.class, Uppercase.class);
assertThat(adapter.toJson("pizza")).isEqualTo("\"|PIZZA|\"");
assertThat(adapter.fromJson("\"|pizza|\"")).isEqualTo("PIZZA");
}
@Test public void toJsonOnly() throws Exception { @Test public void toJsonOnly() throws Exception {
Moshi moshi = new Moshi.Builder() Moshi moshi = new Moshi.Builder()
.add(new PointAsListOfIntegersToAdapter()) .add(new PointAsListOfIntegersToAdapter())
@ -278,65 +183,6 @@ public final class AdapterMethodsTest {
} }
} }
@Test public void emptyAdapters() throws Exception {
Moshi.Builder builder = new Moshi.Builder();
try {
builder.add(new EmptyJsonAdapter()).build();
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage(
"Expected at least one @ToJson or @FromJson method on "
+ "com.squareup.moshi.AdapterMethodsTest$EmptyJsonAdapter");
}
}
static class EmptyJsonAdapter {
}
@Test public void unexpectedSignatureToAdapters() throws Exception {
Moshi.Builder builder = new Moshi.Builder();
try {
builder.add(new UnexpectedSignatureToJsonAdapter()).build();
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("Unexpected signature for void "
+ "com.squareup.moshi.AdapterMethodsTest$UnexpectedSignatureToJsonAdapter.pointToJson"
+ "(com.squareup.moshi.AdapterMethodsTest$Point).\n"
+ "@ToJson method signatures may have one of the following structures:\n"
+ " <any access modifier> void toJson(JsonWriter writer, T value) throws <any>;\n"
+ " <any access modifier> void toJson(JsonWriter writer, T value,"
+ " JsonAdapter<any> delegate, <any more delegates>) throws <any>;\n"
+ " <any access modifier> R toJson(T value) throws <any>;\n");
}
}
static class UnexpectedSignatureToJsonAdapter {
@ToJson void pointToJson(Point point) {
}
}
@Test public void unexpectedSignatureFromAdapters() throws Exception {
Moshi.Builder builder = new Moshi.Builder();
try {
builder.add(new UnexpectedSignatureFromJsonAdapter()).build();
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("Unexpected signature for void "
+ "com.squareup.moshi.AdapterMethodsTest$UnexpectedSignatureFromJsonAdapter.pointFromJson"
+ "(java.lang.String).\n"
+ "@FromJson method signatures may have one of the following structures:\n"
+ " <any access modifier> R fromJson(JsonReader jsonReader) throws <any>;\n"
+ " <any access modifier> R fromJson(JsonReader jsonReader,"
+ " JsonAdapter<any> delegate, <any more delegates>) throws <any>;\n"
+ " <any access modifier> R fromJson(T value) throws <any>;\n");
}
}
static class UnexpectedSignatureFromJsonAdapter {
@FromJson void pointFromJson(String point) {
}
}
/** /**
* Simple adapter methods are not invoked for null values unless they're annotated {@code * Simple adapter methods are not invoked for null values unless they're annotated {@code
* @Nullable}. (The specific annotation class doesn't matter; just its simple name.) * @Nullable}. (The specific annotation class doesn't matter; just its simple name.)
@ -387,33 +233,6 @@ public final class AdapterMethodsTest {
@interface Nullable { @interface Nullable {
} }
@Test public void toAndFromNullJsonWithWriterAndReader() throws Exception {
Moshi moshi = new Moshi.Builder()
.add(new NullableIntToJsonAdapter())
.build();
JsonAdapter<Point> pointAdapter = moshi.adapter(Point.class);
assertThat(pointAdapter.fromJson("{\"x\":null,\"y\":3}")).isEqualTo(new Point(-1, 3));
assertThat(pointAdapter.toJson(new Point(-1, 3))).isEqualTo("{\"y\":3}");
}
static class NullableIntToJsonAdapter {
@FromJson int jsonToInt(JsonReader reader) throws IOException {
if (reader.peek() == JsonReader.Token.NULL) {
reader.nextNull();
return -1;
}
return reader.nextInt();
}
@ToJson void intToJson(JsonWriter writer, int value) throws IOException {
if (value == -1) {
writer.nullValue();
} else {
writer.value(value);
}
}
}
@Test public void adapterThrows() throws Exception { @Test public void adapterThrows() throws Exception {
Moshi moshi = new Moshi.Builder() Moshi moshi = new Moshi.Builder()
.add(new ExceptionThrowingPointJsonAdapter()) .add(new ExceptionThrowingPointJsonAdapter())
@ -437,12 +256,10 @@ public final class AdapterMethodsTest {
static class ExceptionThrowingPointJsonAdapter { static class ExceptionThrowingPointJsonAdapter {
@ToJson void pointToJson(JsonWriter writer, Point point) throws Exception { @ToJson void pointToJson(JsonWriter writer, Point point) throws Exception {
if (point != null) throw new Exception("pointToJson fail!"); throw new Exception("pointToJson fail!");
writer.nullValue();
} }
@FromJson Point pointFromJson(JsonReader reader) throws Exception { @FromJson Point pointFromJson(JsonReader reader) throws Exception {
if (reader.peek() == JsonReader.Token.NULL) return reader.nextNull();
throw new Exception("pointFromJson fail!"); throw new Exception("pointFromJson fail!");
} }
} }
@ -462,10 +279,7 @@ public final class AdapterMethodsTest {
fail(); fail();
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
assertThat(e).hasMessage("No @FromJson adapter for interface " assertThat(e).hasMessage("No @FromJson adapter for interface "
+ "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)"); + "com.squareup.moshi.AdapterMethodsTest$Shape annotated []");
assertThat(e).hasCauseExactlyInstanceOf(IllegalArgumentException.class);
assertThat(e.getCause()).hasMessage("No next JsonAdapter for interface "
+ "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)");
} }
} }
@ -484,284 +298,7 @@ public final class AdapterMethodsTest {
fail(); fail();
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
assertThat(e).hasMessage("No @ToJson adapter for interface " assertThat(e).hasMessage("No @ToJson adapter for interface "
+ "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)"); + "com.squareup.moshi.AdapterMethodsTest$Shape annotated []");
assertThat(e).hasCauseExactlyInstanceOf(IllegalArgumentException.class);
assertThat(e.getCause()).hasMessage("No next JsonAdapter for interface "
+ "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)");
}
}
/**
* Unfortunately in some versions of Android the implementations of {@link ParameterizedType}
* doesn't implement equals and hashCode. Confirm that we work around that.
*/
@Test public void parameterizedTypeEqualsNotUsed() throws Exception {
Moshi moshi = new Moshi.Builder()
.add(new ListOfStringJsonAdapter())
.build();
// This class doesn't implement equals() and hashCode() as it should.
ParameterizedType listOfStringType = brokenParameterizedType(0, List.class, String.class);
JsonAdapter<List<String>> jsonAdapter = moshi.adapter(listOfStringType);
assertThat(jsonAdapter.toJson(Arrays.asList("a", "b", "c"))).isEqualTo("\"a|b|c\"");
assertThat(jsonAdapter.fromJson("\"a|b|c\"")).isEqualTo(Arrays.asList("a", "b", "c"));
}
static class ListOfStringJsonAdapter {
@ToJson String listOfStringToJson(List<String> list) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < list.size(); i++) {
if (i > 0) result.append('|');
result.append(list.get(i));
}
return result.toString();
}
@FromJson List<String> listOfStringFromJson(String string) {
return Arrays.asList(string.split("\\|"));
}
}
/**
* Even when the types we use to look up JSON adapters are not equal, if they're equivalent they
* should return the same JsonAdapter instance.
*/
@Test public void parameterizedTypeCacheKey() throws Exception {
Moshi moshi = new Moshi.Builder().build();
Type a = brokenParameterizedType(0, List.class, String.class);
Type b = brokenParameterizedType(1, List.class, String.class);
Type c = brokenParameterizedType(2, List.class, String.class);
assertThat(moshi.adapter(b)).isSameAs(moshi.adapter(a));
assertThat(moshi.adapter(c)).isSameAs(moshi.adapter(a));
}
@Test public void writerAndReaderTakingJsonAdapterParameter() throws Exception {
Moshi moshi = new Moshi.Builder()
.add(new PointWriterAndReaderJsonAdapter())
.add(new JsonAdapterWithWriterAndReaderTakingJsonAdapterParameter())
.build();
JsonAdapter<Line> lineAdapter = moshi.adapter(Line.class);
Line line = new Line(new Point(5, 8), new Point(3, 2));
assertThat(lineAdapter.toJson(line)).isEqualTo("[[5,8],[3,2]]");
assertThat(lineAdapter.fromJson("[[5,8],[3,2]]")).isEqualTo(line);
}
static class JsonAdapterWithWriterAndReaderTakingJsonAdapterParameter {
@ToJson void lineToJson(
JsonWriter writer, Line line, JsonAdapter<Point> pointAdapter) throws IOException {
writer.beginArray();
pointAdapter.toJson(writer, line.a);
pointAdapter.toJson(writer, line.b);
writer.endArray();
}
@FromJson Line lineFromJson(
JsonReader reader, JsonAdapter<Point> pointAdapter) throws Exception {
reader.beginArray();
Point a = pointAdapter.fromJson(reader);
Point b = pointAdapter.fromJson(reader);
reader.endArray();
return new Line(a, b);
}
}
@Test public void writerAndReaderTakingAnnotatedJsonAdapterParameter() throws Exception {
Moshi moshi = new Moshi.Builder()
.add(new PointWithParensJsonAdapter())
.add(new JsonAdapterWithWriterAndReaderTakingAnnotatedJsonAdapterParameter())
.build();
JsonAdapter<Line> lineAdapter = moshi.adapter(Line.class);
Line line = new Line(new Point(5, 8), new Point(3, 2));
assertThat(lineAdapter.toJson(line)).isEqualTo("[\"(5 8)\",\"(3 2)\"]");
assertThat(lineAdapter.fromJson("[\"(5 8)\",\"(3 2)\"]")).isEqualTo(line);
}
static class PointWithParensJsonAdapter{
@ToJson String pointToJson(@WithParens Point point) throws IOException {
return String.format("(%s %s)", point.x, point.y);
}
@FromJson @WithParens Point pointFromJson(String string) throws Exception {
Matcher matcher = Pattern.compile("\\((\\d+) (\\d+)\\)").matcher(string);
if (!matcher.matches()) throw new JsonDataException();
return new Point(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2)));
}
}
static class JsonAdapterWithWriterAndReaderTakingAnnotatedJsonAdapterParameter {
@ToJson void lineToJson(JsonWriter writer, Line line,
@WithParens JsonAdapter<Point> pointAdapter) throws IOException {
writer.beginArray();
pointAdapter.toJson(writer, line.a);
pointAdapter.toJson(writer, line.b);
writer.endArray();
}
@FromJson Line lineFromJson(
JsonReader reader, @WithParens JsonAdapter<Point> pointAdapter) throws Exception {
reader.beginArray();
Point a = pointAdapter.fromJson(reader);
Point b = pointAdapter.fromJson(reader);
reader.endArray();
return new Line(a, b);
}
}
@Test public void writerAndReaderTakingMultipleJsonAdapterParameters() throws Exception {
Moshi moshi = new Moshi.Builder()
.add(new PointWriterAndReaderJsonAdapter())
.add(new PointWithParensJsonAdapter())
.add(new JsonAdapterWithWriterAndReaderTakingMultipleJsonAdapterParameters())
.build();
JsonAdapter<Line> lineAdapter = moshi.adapter(Line.class);
Line line = new Line(new Point(5, 8), new Point(3, 2));
assertThat(lineAdapter.toJson(line)).isEqualTo("[[5,8],\"(3 2)\"]");
assertThat(lineAdapter.fromJson("[[5,8],\"(3 2)\"]")).isEqualTo(line);
}
static class JsonAdapterWithWriterAndReaderTakingMultipleJsonAdapterParameters {
@ToJson void lineToJson(JsonWriter writer, Line line,
JsonAdapter<Point> aAdapter, @WithParens JsonAdapter<Point> bAdapter) throws IOException {
writer.beginArray();
aAdapter.toJson(writer, line.a);
bAdapter.toJson(writer, line.b);
writer.endArray();
}
@FromJson Line lineFromJson(JsonReader reader,
JsonAdapter<Point> aAdapter, @WithParens JsonAdapter<Point> bAdapter) throws Exception {
reader.beginArray();
Point a = aAdapter.fromJson(reader);
Point b = bAdapter.fromJson(reader);
reader.endArray();
return new Line(a, b);
}
}
@Retention(RUNTIME)
@JsonQualifier
public @interface WithParens {
}
@Test public void noToJsonAdapterTakingJsonAdapterParameter() throws Exception {
try {
new Moshi.Builder().add(new ToJsonAdapterTakingJsonAdapterParameter());
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessageStartingWith("Unexpected signature");
}
}
static class ToJsonAdapterTakingJsonAdapterParameter {
@ToJson String lineToJson(Line line, JsonAdapter<Point> pointAdapter) throws IOException {
throw new AssertionError();
}
}
@Test public void noFromJsonAdapterTakingJsonAdapterParameter() throws Exception {
try {
new Moshi.Builder().add(new FromJsonAdapterTakingJsonAdapterParameter());
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessageStartingWith("Unexpected signature");
}
}
static class FromJsonAdapterTakingJsonAdapterParameter {
@FromJson Line lineFromJson(String value, JsonAdapter<Point> pointAdapter) throws Exception {
throw new AssertionError();
}
}
@Test public void adaptedTypeIsEnclosedParameterizedType() throws Exception {
Moshi moshi = new Moshi.Builder()
.add(new EnclosedParameterizedTypeJsonAdapter())
.build();
JsonAdapter<Box<Point>> boxAdapter = moshi.adapter(Types.newParameterizedTypeWithOwner(
AdapterMethodsTest.class, Box.class, Point.class));
Box<Point> box = new Box<>(new Point(5, 8));
String json = "[{\"x\":5,\"y\":8}]";
assertThat(boxAdapter.toJson(box)).isEqualTo(json);
assertThat(boxAdapter.fromJson(json)).isEqualTo(box);
}
static class EnclosedParameterizedTypeJsonAdapter {
@FromJson Box<Point> boxFromJson(List<Point> points) {
return new Box<>(points.get(0));
}
@ToJson List<Point> boxToJson(Box<Point> box) throws Exception {
return Collections.singletonList(box.data);
}
}
static class Box<T> {
final T data;
public Box(T data) {
this.data = data;
}
@Override public boolean equals(Object o) {
return o instanceof Box && ((Box) o).data.equals(data);
}
@Override public int hashCode() {
return data.hashCode();
}
}
@Test public void genericArrayTypes() throws Exception {
Moshi moshi = new Moshi.Builder()
.add(new ByteArrayJsonAdapter())
.build();
JsonAdapter<MapOfByteArrays> jsonAdapter = moshi.adapter(MapOfByteArrays.class);
MapOfByteArrays mapOfByteArrays = new MapOfByteArrays(
Collections.singletonMap("a", new byte[] { 0, -1}));
String json = "{\"map\":{\"a\":\"00ff\"}}";
assertThat(jsonAdapter.toJson(mapOfByteArrays)).isEqualTo(json);
assertThat(jsonAdapter.fromJson(json)).isEqualTo(mapOfByteArrays);
}
static class ByteArrayJsonAdapter {
@ToJson String byteArrayToJson(byte[] b) {
return ByteString.of(b).hex();
}
@FromJson byte[] byteArrayFromJson(String s) throws Exception {
return ByteString.decodeHex(s).toByteArray();
}
}
static class MapOfByteArrays {
final Map<String, byte[]> map;
public MapOfByteArrays(Map<String, byte[]> map) {
this.map = map;
}
@Override public boolean equals(Object o) {
return o instanceof MapOfByteArrays && o.toString().equals(toString());
}
@Override public int hashCode() {
return toString().hashCode();
}
@Override public String toString() {
StringBuilder result = new StringBuilder();
for (Map.Entry<String, byte[]> entry : map.entrySet()) {
if (result.length() > 0) result.append(", ");
result.append(entry.getKey())
.append(":")
.append(Arrays.toString(entry.getValue()));
}
return result.toString();
} }
} }
@ -783,55 +320,7 @@ public final class AdapterMethodsTest {
} }
} }
static class Line {
final Point a;
final Point b;
public Line(Point a, Point b) {
this.a = a;
this.b = b;
}
@Override public boolean equals(Object o) {
return o instanceof Line && ((Line) o).a.equals(a) && ((Line) o).b.equals(b);
}
@Override public int hashCode() {
return a.hashCode() * 37 + b.hashCode();
}
}
interface Shape { interface Shape {
String draw(); String draw();
} }
/**
* Returns a new parameterized type that doesn't implement {@link Object#equals} or {@link
* Object#hashCode} by value. These implementation defects are consistent with the parameterized
* type that shipped in some older versions of Android.
*/
ParameterizedType brokenParameterizedType(
final int hashCode, final Class<?> rawType, final Type... typeArguments) {
return new ParameterizedType() {
@Override public Type[] getActualTypeArguments() {
return typeArguments;
}
@Override public Type getRawType() {
return rawType;
}
@Override public Type getOwnerType() {
return null;
}
@Override public boolean equals(Object other) {
return other == this;
}
@Override public int hashCode() {
return hashCode;
}
};
}
} }

View file

@ -15,7 +15,6 @@
*/ */
package com.squareup.moshi; package com.squareup.moshi;
import com.squareup.moshi.internal.Util;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;

View file

@ -25,7 +25,7 @@ import okio.Buffer;
import org.junit.Test; import org.junit.Test;
import static com.squareup.moshi.TestUtil.newReader; import static com.squareup.moshi.TestUtil.newReader;
import static com.squareup.moshi.internal.Util.NO_ANNOTATIONS; import static com.squareup.moshi.Util.NO_ANNOTATIONS;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
@ -341,23 +341,8 @@ public final class ClassJsonAdapterTest {
} }
} }
@Test public void localClassNotSupported() throws Exception {
class Local {
}
try {
ClassJsonAdapter.FACTORY.create(Local.class, NO_ANNOTATIONS, moshi);
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("Cannot serialize local class "
+ "com.squareup.moshi.ClassJsonAdapterTest$1Local");
}
}
interface Interface {
}
@Test public void interfaceNotSupported() throws Exception { @Test public void interfaceNotSupported() throws Exception {
assertThat(ClassJsonAdapter.FACTORY.create(Interface.class, NO_ANNOTATIONS, moshi)).isNull(); assertThat(ClassJsonAdapter.FACTORY.create(Runnable.class, NO_ANNOTATIONS, moshi)).isNull();
} }
static abstract class Abstract { static abstract class Abstract {
@ -444,23 +429,6 @@ public final class ClassJsonAdapterTest {
assertThat(fromJson.zipCode).isEqualTo("94043"); assertThat(fromJson.zipCode).isEqualTo("94043");
} }
static final class Box<T> {
final T data;
Box(T data) {
this.data = data;
}
}
@Test public void parameterizedType() throws Exception {
@SuppressWarnings("unchecked")
JsonAdapter<Box<Integer>> adapter = (JsonAdapter<Box<Integer>>) ClassJsonAdapter.FACTORY.create(
Types.newParameterizedTypeWithOwner(ClassJsonAdapterTest.class, Box.class, Integer.class),
NO_ANNOTATIONS, moshi);
assertThat(adapter.fromJson("{\"data\":5}").data).isEqualTo(5);
assertThat(adapter.toJson(new Box<>(5))).isEqualTo("{\"data\":5}");
}
private <T> String toJson(Class<T> type, T value) throws IOException { private <T> String toJson(Class<T> type, T value) throws IOException {
@SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument. @SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument.
JsonAdapter<T> jsonAdapter = (JsonAdapter<T>) ClassJsonAdapter.FACTORY.create( JsonAdapter<T> jsonAdapter = (JsonAdapter<T>) ClassJsonAdapter.FACTORY.create(

View file

@ -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;
}
}
}

View file

@ -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\"}]");
}
}

View file

@ -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);
}
}

View file

@ -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