Compare commits

..

No commits in common. "master" and "moshi_14" have entirely different histories.

112 changed files with 962 additions and 10964 deletions

View file

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

1
.gitignore vendored
View file

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

View file

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

View file

@ -1,160 +1,6 @@
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_
@ -187,8 +33,8 @@ your application classes.
format of the encoded JSON.
* New: `JsonReader.selectName()` and `selectString()` optimize decoding JSON with known names and
values.
* New: `Types.nextAnnotations()` reduces the amount of code required to implement a custom
`JsonAdapter.Factory`.
* New: `Types.nextAnnotations()` and `Types.createJsonQualifierImplementation()` reduce 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
@ -226,7 +72,7 @@ _2016-10-15_
`StackOverflowError` due to excessive recursion.
* Fix: Require enclosed types to specify their enclosing type with
`Types.newParameterizedTypeWithOwner()`. Previously this API did not exist and looking up
adapters for enclosed parameterized types was not possible.
adapters for enclosed parameterized types as 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.
@ -288,7 +134,7 @@ _2015-09-27_
code should keep a reference to required adapters in a field.
* New: The `Types` factory class makes it possible to compose types like `List<Card>` or
`Map<String, Integer>`. This is useful to look up JSON adapters for parameterized types.
* New: `JsonAdapter.failOnUnknown()` returns a new JSON adapter that throws if an 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
names. This feature shouldnt be used in production because it makes migrations very difficult.
@ -312,4 +158,3 @@ _2015-06-16_
[rfc_7159]: https://tools.ietf.org/html/rfc7159
[gson]: https://github.com/google/gson
[jackson]: http://wiki.fasterxml.com/JacksonHome
[maven_provided]: https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html

159
README.md
View file

@ -120,7 +120,7 @@ Moshi moshi = new Moshi.Builder()
.build();
```
Voilà:
Voila:
```json
{
@ -213,53 +213,20 @@ 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
```java
[
{
"rank": "4",
"suit": "CLUBS"
},
{
"rank": "A",
"suit": "HEARTS"
}
{
"rank": "4",
"suit": "CLUBS"
},
{
"rank": "A",
"suit": "HEARTS"
}
]
```
@ -505,121 +472,26 @@ public final class BlackjackHand {
}
```
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 [the latest JAR][dl] or depend via Maven:
```xml
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi</artifactId>
<version>1.8.0</version>
<version>1.3.1</version>
</dependency>
```
or Gradle:
```kotlin
implementation("com.squareup.moshi:moshi:1.8.0")
```groovy
compile 'com.squareup.moshi:moshi:1.3.1'
```
Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap].
R8 / ProGuard
--------
If you are using R8 or ProGuard add the options from [this file](https://github.com/square/moshi/blob/master/moshi/src/main/resources/META-INF/proguard/moshi.pro). If using Android, this requires Android Gradle Plugin 3.2.0+.
The `moshi-kotlin` artifact additionally requires the options from [this file](https://github.com/square/moshi/blob/master/kotlin/reflect/src/main/resources/META-INF/proguard/moshi-kotlin.pro)
You might also need rules for Okio which is a dependency of this library.
License
--------
@ -638,10 +510,9 @@ License
limitations under the License.
[dl]: https://search.maven.org/classic/remote_content?g=com.squareup.moshi&a=moshi&v=LATEST
[dl]: https://search.maven.org/remote_content?g=com.squareup.moshi&a=moshi&v=LATEST
[snap]: https://oss.sonatype.org/content/repositories/snapshots/com/squareup/moshi/
[okio]: https://github.com/square/okio/
[okhttp]: https://github.com/square/okhttp/
[gson]: https://github.com/google/gson/
[javadoc]: https://square.github.io/moshi/1.x/moshi/
[kapt]: https://kotlinlang.org/docs/reference/kapt.html
[javadoc]: http://square.github.io/moshi/1.x/moshi/

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

View file

@ -13,9 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi.adapters;
package com.squareup.moshi;
import com.squareup.moshi.JsonDataException;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
@ -24,7 +23,7 @@ import java.util.TimeZone;
/**
* 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
* friendly than using SimpleDateFormat so highly suitable if you (un)serialize lots of date
@ -191,7 +190,7 @@ final class Iso8601Utils {
// If we get a ParseException it'll already have the right message/offset.
// Other exception types can convert here.
} catch (IndexOutOfBoundsException | IllegalArgumentException e) {
throw new JsonDataException("Not an RFC 3339 date: " + date, e);
throw new JsonDataException("Not an RFC 3339 date: " + date);
}
}

View file

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

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
* limitations under the License.
*/
package com.squareup.moshi.adapters;
package com.squareup.moshi;
import com.squareup.moshi.JsonAdapter;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

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

View file

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

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

@ -21,7 +21,7 @@ import com.squareup.moshi.recipes.models.Player;
public final class CustomFieldName {
public void run() throws Exception {
String json = ""
String json = ""
+ "{"
+ " \"username\": \"jesse\","
+ " \"lucky number\": 32"

View file

@ -24,7 +24,6 @@ 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;
@ -36,20 +35,17 @@ public final class DefaultOnDataMismatchAdapter<T> extends JsonAdapter<T> {
}
@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;
// Read the value first so that the reader will be in a known state even if there's an
// exception. Otherwise it may be awkward to recover: it might be between calls to
// beginObject() and endObject() for example.
Object jsonValue = reader.readJsonValue();
// Use the delegate to convert the JSON value to the target type.
try {
// Attempt to decode to the target type with the peeked reader.
result = delegate.fromJson(peeked);
return delegate.fromJsonValue(jsonValue);
} catch (JsonDataException e) {
result = defaultValue;
} finally {
peeked.close();
return defaultValue;
}
// 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 {
@ -58,7 +54,7 @@ public final class DefaultOnDataMismatchAdapter<T> extends JsonAdapter<T> {
public static <T> Factory newFactory(final Class<T> type, final T defaultValue) {
return new Factory() {
@Override public @Nullable JsonAdapter<?> create(
@Override public JsonAdapter<?> create(
Type requestedType, Set<? extends Annotation> annotations, Moshi moshi) {
if (type != requestedType) return null;
JsonAdapter<T> delegate = moshi.nextAdapter(this, type, annotations);

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

@ -44,7 +44,6 @@ public final class FromJsonWithoutStrings {
new FromJsonWithoutStrings().run();
}
@SuppressWarnings("checkstyle:membername")
private static final class EventJson {
String title;
String begin_date;
@ -56,10 +55,10 @@ public final class FromJsonWithoutStrings {
String beginDateAndTime;
@Override public String toString() {
return "Event{"
+ "title='" + title + '\''
+ ", beginDateAndTime='" + beginDateAndTime + '\''
+ '}';
return "Event{" +
"title='" + title + '\'' +
", beginDateAndTime='" + beginDateAndTime + '\'' +
'}';
}
}

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.Moshi;
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter;
import com.squareup.moshi.Rfc3339DateJsonAdapter;
import com.squareup.moshi.recipes.models.Tournament;
import java.util.Calendar;
import java.util.Date;

View file

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

View file

@ -28,14 +28,10 @@ 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\":"
@ -52,8 +48,8 @@ final class Unwrap {
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) {
@Override
public JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi) {
Set<? extends Annotation> delegateAnnotations =
Types.nextAnnotations(annotations, Enveloped.class);
if (delegateAnnotations == null) {
@ -88,7 +84,7 @@ final class Unwrap {
}
@Override public void toJson(JsonWriter writer, Object value) throws IOException {
delegate.toJson(writer, new Envelope<>(value));
delegate.toJson(new Envelope<>(value));
}
}
}

View file

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

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

View file

@ -15,7 +15,6 @@
*/
package com.squareup.moshi;
import com.squareup.moshi.internal.Util;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
@ -25,22 +24,19 @@ import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
import static com.squareup.moshi.internal.Util.canonicalize;
import static com.squareup.moshi.internal.Util.jsonAnnotations;
import static com.squareup.moshi.internal.Util.typeAnnotatedWithAnnotations;
import static com.squareup.moshi.Util.jsonAnnotations;
final class AdapterMethodsFactory implements JsonAdapter.Factory {
private final List<AdapterMethod> toAdapters;
private final List<AdapterMethod> fromAdapters;
AdapterMethodsFactory(List<AdapterMethod> toAdapters, List<AdapterMethod> fromAdapters) {
public AdapterMethodsFactory(List<AdapterMethod> toAdapters, List<AdapterMethod> fromAdapters) {
this.toAdapters = toAdapters;
this.fromAdapters = fromAdapters;
}
@Override public @Nullable JsonAdapter<?> create(
@Override public JsonAdapter<?> create(
final Type type, final Set<? extends Annotation> annotations, final Moshi moshi) {
final AdapterMethod toAdapter = get(toAdapters, type, annotations);
final AdapterMethod fromAdapter = get(fromAdapters, type, annotations);
@ -53,17 +49,17 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
} catch (IllegalArgumentException e) {
String missingAnnotation = toAdapter == null ? "@ToJson" : "@FromJson";
throw new IllegalArgumentException("No " + missingAnnotation + " adapter for "
+ typeAnnotatedWithAnnotations(type, annotations), e);
+ type + " annotated " + annotations);
}
} else {
delegate = null;
}
if (toAdapter != null) toAdapter.bind(moshi, this);
if (fromAdapter != null) fromAdapter.bind(moshi, this);
if (toAdapter != null) toAdapter.bind(moshi);
if (fromAdapter != null) fromAdapter.bind(moshi);
return new JsonAdapter<Object>() {
@Override public void toJson(JsonWriter writer, @Nullable Object value) throws IOException {
@Override public void toJson(JsonWriter writer, Object value) throws IOException {
if (toAdapter == null) {
delegate.toJson(writer, value);
} else if (!toAdapter.nullable && value == null) {
@ -79,7 +75,7 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
}
}
@Override public @Nullable Object fromJson(JsonReader reader) throws IOException {
@Override public Object fromJson(JsonReader reader) throws IOException {
if (fromAdapter == null) {
return delegate.fromJson(reader);
} else if (!fromAdapter.nullable && reader.peek() == JsonReader.Token.NULL) {
@ -159,7 +155,7 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
Set<? extends Annotation> qualifierAnnotations = jsonAnnotations(parameterAnnotations[1]);
return new AdapterMethod(parameterTypes[1], qualifierAnnotations, adapter, method,
parameterTypes.length, 2, true) {
@Override public void toJson(Moshi moshi, JsonWriter writer, @Nullable Object value)
@Override public void toJson(Moshi moshi, JsonWriter writer, Object value)
throws IOException, InvocationTargetException {
invoke(writer, value);
}
@ -168,22 +164,18 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
} else if (parameterTypes.length == 1 && returnType != void.class) {
// List<Integer> pointToJson(Point point) {
final Set<? extends Annotation> returnTypeAnnotations = jsonAnnotations(method);
final Set<? extends Annotation> qualifierAnnotations =
jsonAnnotations(parameterAnnotations[0]);
Set<? extends Annotation> qualifierAnnotations = jsonAnnotations(parameterAnnotations[0]);
boolean nullable = Util.hasNullable(parameterAnnotations[0]);
return new AdapterMethod(parameterTypes[0], qualifierAnnotations, adapter, method,
parameterTypes.length, 1, nullable) {
private JsonAdapter<Object> delegate;
@Override public void bind(Moshi moshi, JsonAdapter.Factory factory) {
super.bind(moshi, factory);
delegate = Types.equals(parameterTypes[0], returnType)
&& qualifierAnnotations.equals(returnTypeAnnotations)
? moshi.nextAdapter(factory, returnType, returnTypeAnnotations)
: moshi.adapter(returnType, returnTypeAnnotations);
@Override public void bind(Moshi moshi) {
super.bind(moshi);
delegate = moshi.adapter(returnType, returnTypeAnnotations);
}
@Override public void toJson(Moshi moshi, JsonWriter writer, @Nullable Object value)
@Override public void toJson(Moshi moshi, JsonWriter writer, Object value)
throws IOException, InvocationTargetException {
Object intermediate = invoke(value);
delegate.toJson(writer, intermediate);
@ -194,8 +186,6 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
throw new IllegalArgumentException("Unexpected signature for " + method + ".\n"
+ "@ToJson method signatures may have one of the following structures:\n"
+ " <any access modifier> void toJson(JsonWriter writer, T value) throws <any>;\n"
+ " <any access modifier> void toJson(JsonWriter writer, T value,"
+ " JsonAdapter<any> delegate, <any more delegates>) throws <any>;\n"
+ " <any access modifier> R toJson(T value) throws <any>;\n");
}
}
@ -216,7 +206,7 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
static AdapterMethod fromAdapter(Object adapter, Method method) {
method.setAccessible(true);
final Type returnType = method.getGenericReturnType();
final Set<? extends Annotation> returnTypeAnnotations = jsonAnnotations(method);
Set<? extends Annotation> returnTypeAnnotations = jsonAnnotations(method);
final Type[] parameterTypes = method.getGenericParameterTypes();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
@ -243,12 +233,9 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
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 void bind(Moshi moshi) {
super.bind(moshi);
delegate = moshi.adapter(parameterTypes[0], qualifierAnnotations);
}
@Override public Object fromJson(Moshi moshi, JsonReader reader)
@ -261,26 +248,24 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
} else {
throw new IllegalArgumentException("Unexpected signature for " + method + ".\n"
+ "@FromJson method signatures may have one of the following structures:\n"
+ " <any access modifier> 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> void fromJson(JsonReader jsonReader) throws <any>;\n"
+ " <any access modifier> R fromJson(T value) throws <any>;\n");
}
}
/** Returns the matching adapter method from the list. */
private static @Nullable AdapterMethod get(
private static AdapterMethod get(
List<AdapterMethod> adapterMethods, Type type, Set<? extends Annotation> annotations) {
for (int i = 0, size = adapterMethods.size(); i < size; i++) {
AdapterMethod adapterMethod = adapterMethods.get(i);
if (Types.equals(adapterMethod.type, type) && adapterMethod.annotations.equals(annotations)) {
if (adapterMethod.type.equals(type) && adapterMethod.annotations.equals(annotations)) {
return adapterMethod;
}
}
return null;
}
abstract static class AdapterMethod {
static abstract class AdapterMethod {
final Type type;
final Set<? extends Annotation> annotations;
final Object adapter;
@ -289,9 +274,9 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
final JsonAdapter<?>[] jsonAdapters;
final boolean nullable;
AdapterMethod(Type type, Set<? extends Annotation> annotations, Object adapter,
public AdapterMethod(Type type, Set<? extends Annotation> annotations, Object adapter,
Method method, int parameterCount, int adaptersOffset, boolean nullable) {
this.type = canonicalize(type);
this.type = Types.canonicalize(type);
this.annotations = annotations;
this.adapter = adapter;
this.method = method;
@ -300,33 +285,30 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
this.nullable = nullable;
}
public void bind(Moshi moshi, JsonAdapter.Factory factory) {
public void bind(Moshi moshi) {
if (jsonAdapters.length > 0) {
Type[] parameterTypes = method.getGenericParameterTypes();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
for (int i = adaptersOffset, size = parameterTypes.length; i < size; i++) {
Type type = ((ParameterizedType) parameterTypes[i]).getActualTypeArguments()[0];
Set<? extends Annotation> jsonAnnotations = jsonAnnotations(parameterAnnotations[i]);
jsonAdapters[i - adaptersOffset] =
Types.equals(this.type, type) && annotations.equals(jsonAnnotations)
? moshi.nextAdapter(factory, type, jsonAnnotations)
: moshi.adapter(type, jsonAnnotations);
jsonAdapters[i - adaptersOffset] = moshi.adapter(
((ParameterizedType) parameterTypes[i]).getActualTypeArguments()[0],
jsonAnnotations(parameterAnnotations[i]));
}
}
}
public void toJson(Moshi moshi, JsonWriter writer, @Nullable Object value)
public void toJson(Moshi moshi, JsonWriter writer, Object value)
throws IOException, InvocationTargetException {
throw new AssertionError();
}
public @Nullable Object fromJson(Moshi moshi, JsonReader reader)
public Object fromJson(Moshi moshi, JsonReader reader)
throws IOException, InvocationTargetException {
throw new AssertionError();
}
/** Invoke the method with one fixed argument, plus any number of JSON adapter arguments. */
protected @Nullable Object invoke(@Nullable Object a1) throws InvocationTargetException {
protected Object invoke(Object a1) throws InvocationTargetException {
Object[] args = new Object[1 + jsonAdapters.length];
args[0] = a1;
System.arraycopy(jsonAdapters, 0, args, 1, jsonAdapters.length);
@ -339,8 +321,7 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
}
/** 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 {
protected Object invoke(Object a1, Object a2) throws InvocationTargetException {
Object[] args = new Object[2 + jsonAdapters.length];
args[0] = a1;
args[1] = a2;

View file

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

View file

@ -15,7 +15,6 @@
*/
package com.squareup.moshi;
import com.squareup.moshi.internal.Util;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.lang.reflect.Constructor;
@ -104,7 +103,7 @@ abstract class ClassFactory<T> {
} catch (IllegalAccessException e) {
throw new AssertionError();
} catch (InvocationTargetException e) {
throw Util.rethrowCause(e);
throw new RuntimeException(e);
} catch (NoSuchMethodException ignored) {
// Not the expected version of Dalvik/libcore!
}

View file

@ -15,20 +15,15 @@
*/
package com.squareup.moshi;
import com.squareup.moshi.internal.Util;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import javax.annotation.Nullable;
import static com.squareup.moshi.internal.Util.resolve;
/**
* Emits a regular class as a JSON object by mapping Java fields to JSON object properties.
@ -47,45 +42,31 @@ import static com.squareup.moshi.internal.Util.resolve;
*/
final class ClassJsonAdapter<T> extends JsonAdapter<T> {
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
@Override public @Nullable JsonAdapter<?> create(
@Override public JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
if (!(type instanceof Class) && !(type instanceof ParameterizedType)) {
return null;
}
Class<?> rawType = Types.getRawType(type);
if (rawType.isInterface() || rawType.isEnum()) return null;
if (isPlatformType(rawType) && !Types.isAllowedPlatformType(rawType)) {
throw new IllegalArgumentException("Platform "
+ type
+ " annotated "
+ annotations
+ " requires explicit JsonAdapter to be registered");
}
if (!annotations.isEmpty()) return null;
if (Util.isPlatformType(rawType)) {
throw new IllegalArgumentException(
"Platform " + type + " requires explicit JsonAdapter to be registered");
}
if (rawType.isAnonymousClass()) {
throw new IllegalArgumentException("Cannot serialize anonymous class " + rawType.getName());
}
if (rawType.isLocalClass()) {
throw new IllegalArgumentException("Cannot serialize local class " + rawType.getName());
}
if (rawType.getEnclosingClass() != null && !Modifier.isStatic(rawType.getModifiers())) {
throw new IllegalArgumentException(
"Cannot serialize non-static nested class " + rawType.getName());
if (rawType.getSimpleName().isEmpty()) {
throw new IllegalArgumentException(
"Cannot serialize anonymous class " + rawType.getName());
} else {
throw new IllegalArgumentException(
"Cannot serialize non-static nested class " + rawType.getName());
}
}
if (Modifier.isAbstract(rawType.getModifiers())) {
throw new IllegalArgumentException("Cannot serialize abstract class " + rawType.getName());
}
try {
//noinspection unchecked if the Class.forName works, the cast will work.
Class<? extends Annotation> metadataClass =
(Class<? extends Annotation>) Class.forName("kotlin.Metadata");
if (rawType.isAnnotationPresent(metadataClass)) {
throw new IllegalArgumentException("Cannot serialize Kotlin type " + rawType.getName()
+ ". Reflective serialization of Kotlin classes without using kotlin-reflect has "
+ "undefined and unexpected behavior. Please use KotlinJsonAdapter from the "
+ "moshi-kotlin artifact or use code gen from the moshi-kotlin-codegen artifact.");
}
} catch (ClassNotFoundException ignored) {
}
ClassFactory<Object> classFactory = ClassFactory.get(rawType);
Map<String, FieldBinding<?>> fields = new TreeMap<>();
@ -99,22 +80,21 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
private void createFieldBindings(
Moshi moshi, Type type, Map<String, FieldBinding<?>> fieldBindings) {
Class<?> rawType = Types.getRawType(type);
boolean platformType = Util.isPlatformType(rawType);
boolean platformType = isPlatformType(rawType);
for (Field field : rawType.getDeclaredFields()) {
if (!includeField(platformType, field.getModifiers())) continue;
// Look up a type adapter for this type.
Type fieldType = resolve(type, rawType, field.getGenericType());
Type fieldType = Types.resolve(type, rawType, field.getGenericType());
Set<? extends Annotation> annotations = Util.jsonAnnotations(field);
String fieldName = field.getName();
JsonAdapter<Object> adapter = moshi.adapter(fieldType, annotations, fieldName);
JsonAdapter<Object> adapter = moshi.adapter(fieldType, annotations);
// Create the binding between field and JSON.
field.setAccessible(true);
// Store it using the field's name. If there was already a field with this name, fail!
Json jsonAnnotation = field.getAnnotation(Json.class);
String name = jsonAnnotation != null ? jsonAnnotation.name() : fieldName;
String name = jsonAnnotation != null ? jsonAnnotation.name() : field.getName();
FieldBinding<Object> fieldBinding = new FieldBinding<>(name, field, adapter);
FieldBinding<?> replaced = fieldBindings.put(name, fieldBinding);
if (replaced != null) {
@ -125,6 +105,19 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
}
}
/**
* Returns true if {@code rawType} is built in. We don't reflect on private fields of platform
* types because they're unspecified and likely to be different on Java vs. Android.
*/
private boolean isPlatformType(Class<?> rawType) {
String name = rawType.getName();
return name.startsWith("android.")
|| name.startsWith("java.")
|| name.startsWith("javax.")
|| name.startsWith("kotlin.")
|| name.startsWith("scala.");
}
/** Returns true if fields with {@code modifiers} are included in the emitted JSON. */
private boolean includeField(boolean platformType, int modifiers) {
if (Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers)) return false;
@ -150,7 +143,10 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw Util.rethrowCause(e);
Throwable targetException = e.getTargetException();
if (targetException instanceof RuntimeException) throw (RuntimeException) targetException;
if (targetException instanceof Error) throw (Error) targetException;
throw new RuntimeException(targetException);
} catch (IllegalAccessException e) {
throw new AssertionError();
}
@ -159,12 +155,15 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
reader.beginObject();
while (reader.hasNext()) {
int index = reader.selectName(options);
if (index == -1) {
reader.skipName();
FieldBinding<?> fieldBinding;
if (index != -1) {
fieldBinding = fieldsArray[index];
} else {
reader.nextName();
reader.skipValue();
continue;
}
fieldsArray[index].read(reader, result);
fieldBinding.read(reader, result);
}
reader.endObject();
return result;
@ -195,7 +194,7 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
final Field field;
final JsonAdapter<T> adapter;
FieldBinding(String name, Field field, JsonAdapter<T> adapter) {
public FieldBinding(String name, Field field, JsonAdapter<T> adapter) {
this.name = name;
this.field = field;
this.adapter = adapter;

View file

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

View file

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

View file

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

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;
import javax.annotation.Nullable;
/**
* Thrown when the data in a JSON document doesn't match the data expected by the caller. For
* example, suppose the application expects a boolean but the JSON document contains a string. When
@ -33,15 +31,15 @@ public final class JsonDataException extends RuntimeException {
public JsonDataException() {
}
public JsonDataException(@Nullable String message) {
public JsonDataException(String message) {
super(message);
}
public JsonDataException(@Nullable Throwable cause) {
public JsonDataException(Throwable cause) {
super(cause);
}
public JsonDataException(@Nullable String message, @Nullable Throwable cause) {
public JsonDataException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -16,11 +16,10 @@
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) {
public JsonEncodingException(String message) {
super(message);
}
}

View file

@ -18,11 +18,8 @@ package com.squareup.moshi;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import okio.Buffer;
import okio.BufferedSource;
import okio.ByteString;
@ -121,7 +118,7 @@ import okio.ByteString;
* id = reader.nextLong();
* } else if (name.equals("text")) {
* text = reader.nextString();
* } else if (name.equals("geo") && reader.peek() != Token.NULL) {
* } else if (name.equals("geo") && reader.peek() != JsonToken.NULL) {
* geo = readDoublesArray(reader);
* } else if (name.equals("user")) {
* user = readUser(reader);
@ -177,50 +174,32 @@ import okio.ByteString;
* of this class are not thread safe.
*/
public abstract class JsonReader implements Closeable {
// The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack will
// grow itself up to 256 levels of nesting including the top-level document. Deeper nesting is
// prone to trigger StackOverflowErrors.
int stackSize;
int[] scopes;
String[] pathNames;
int[] pathIndices;
// The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack permits
// up to 32 levels of nesting including the top-level document. Deeper nesting is prone to trigger
// StackOverflowErrors.
int stackSize = 0;
final int[] scopes = new int[32];
final String[] pathNames = new String[32];
final int[] pathIndices = new int[32];
/** True to accept non-spec compliant JSON. */
/** True to accept non-spec compliant JSON */
boolean lenient;
/** True to throw a {@link JsonDataException} on any attempt to call {@link #skipValue()}. */
boolean failOnUnknown;
/** Returns a new instance that reads UTF-8 encoded JSON from {@code source}. */
@CheckReturnValue public static JsonReader of(BufferedSource source) {
public static JsonReader of(BufferedSource source) {
return new JsonUtf8Reader(source);
}
// Package-private to control subclasses.
JsonReader() {
scopes = new int[32];
pathNames = new String[32];
pathIndices = new int[32];
}
// Package-private to control subclasses.
JsonReader(JsonReader copyFrom) {
this.stackSize = copyFrom.stackSize;
this.scopes = copyFrom.scopes.clone();
this.pathNames = copyFrom.pathNames.clone();
this.pathIndices = copyFrom.pathIndices.clone();
this.lenient = copyFrom.lenient;
this.failOnUnknown = copyFrom.failOnUnknown;
// Package-private to control subclasses.
}
final void pushScope(int newTop) {
if (stackSize == scopes.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);
throw new JsonDataException("Nesting too deep at " + getPath());
}
scopes[stackSize++] = newTop;
}
@ -233,7 +212,7 @@ public abstract class JsonReader implements Closeable {
throw new JsonEncodingException(message + " at path " + getPath());
}
final JsonDataException typeMismatch(@Nullable Object value, Object expected) {
final JsonDataException typeMismatch(Object value, Object expected) {
if (value == null) {
return new JsonDataException(
"Expected " + expected + " but was null at path " + getPath());
@ -275,7 +254,7 @@ public abstract class JsonReader implements Closeable {
/**
* Returns true if this parser is liberal in what it accepts.
*/
@CheckReturnValue public final boolean isLenient() {
public final boolean isLenient() {
return lenient;
}
@ -292,9 +271,9 @@ public abstract class JsonReader implements Closeable {
}
/**
* Returns true if this parser forbids skipping names and values.
* Returns true if this parser forbids skipping values.
*/
@CheckReturnValue public final boolean failOnUnknown() {
public final boolean failOnUnknown() {
return failOnUnknown;
}
@ -325,34 +304,25 @@ public abstract class JsonReader implements Closeable {
/**
* Returns true if the current array or object has another element.
*/
@CheckReturnValue public abstract boolean hasNext() throws IOException;
public abstract boolean hasNext() throws IOException;
/**
* Returns the type of the next token without consuming it.
*/
@CheckReturnValue public abstract Token peek() throws IOException;
public abstract Token peek() throws IOException;
/**
* Returns the next token, a {@linkplain Token#NAME property name}, and consumes it.
*
* @throws JsonDataException if the next token in the stream is not a property name.
*/
@CheckReturnValue public abstract String nextName() throws IOException;
public abstract String nextName() throws IOException;
/**
* If the next token is a {@linkplain Token#NAME property name} that's in {@code options}, this
* consumes it and returns its index. Otherwise this returns -1 and no name is consumed.
*/
@CheckReturnValue public abstract int selectName(Options options) throws IOException;
/**
* Skips the next token, consuming it. This method is intended for use when the JSON token stream
* contains unrecognized or unhandled names.
*
* <p>This throws a {@link JsonDataException} if this parser has been configured to {@linkplain
* #failOnUnknown fail on unknown} names.
*/
public abstract void skipName() throws IOException;
public abstract int selectName(Options options) throws IOException;
/**
* Returns the {@linkplain Token#STRING string} value of the next token, consuming it. If the next
@ -366,7 +336,7 @@ public abstract class JsonReader implements Closeable {
* If the next token is a {@linkplain Token#STRING string} that's in {@code options}, this
* consumes it and returns its index. Otherwise this returns -1 and no string is consumed.
*/
@CheckReturnValue public abstract int selectString(Options options) throws IOException;
public abstract int selectString(Options options) throws IOException;
/**
* Returns the {@linkplain Token#BOOLEAN boolean} value of the next token, consuming it.
@ -381,7 +351,7 @@ public abstract class JsonReader implements Closeable {
*
* @throws JsonDataException if the next token is not null or if this reader is closed.
*/
public abstract @Nullable <T> T nextNull() throws IOException;
public abstract <T> T nextNull() throws IOException;
/**
* Returns the {@linkplain Token#NUMBER double} value of the next token, consuming it. If the next
@ -430,7 +400,7 @@ public abstract class JsonReader implements Closeable {
* @throws JsonDataException if the next token is not a literal value, if a JSON object has a
* duplicate key.
*/
public final @Nullable Object readJsonValue() throws IOException {
public final Object readJsonValue() throws IOException {
switch (peek()) {
case BEGIN_ARRAY:
List<Object> list = new ArrayList<>();
@ -474,37 +444,11 @@ public abstract class JsonReader implements Closeable {
}
}
/**
* Returns a new {@code JsonReader} that can read data from this {@code JsonReader} without
* consuming it. The returned reader becomes invalid once this one is next read or closed.
*
* <p>For example, we can use {@code peekJson()} to lookahead and read the same data multiple
* times.
*
* <pre> {@code
*
* Buffer buffer = new Buffer();
* buffer.writeUtf8("[123, 456, 789]")
*
* JsonReader jsonReader = JsonReader.of(buffer);
* jsonReader.beginArray();
* jsonReader.nextInt(); // Returns 123, reader contains 456, 789 and ].
*
* JsonReader peek = reader.peekJson();
* peek.nextInt() // Returns 456.
* peek.nextInt() // Returns 789.
* peek.endArray()
*
* jsonReader.nextInt() // Returns 456, reader contains 789 and ].
* }</pre>
*/
@CheckReturnValue public abstract JsonReader peekJson();
/**
* Returns a <a href="http://goessner.net/articles/JsonPath/">JsonPath</a> to
* the current location in the JSON value.
*/
@CheckReturnValue public final String getPath() {
public final String getPath() {
return JsonScope.getPath(stackSize, scopes, pathNames, pathIndices);
}
@ -527,7 +471,7 @@ public abstract class JsonReader implements Closeable {
this.doubleQuoteSuffix = doubleQuoteSuffix;
}
@CheckReturnValue public static Options of(String... strings) {
public static Options of(String... strings) {
try {
ByteString[] result = new ByteString[strings.length];
Buffer buffer = new Buffer();

View file

@ -17,8 +17,6 @@ package com.squareup.moshi;
/** Lexical scoping elements within a JSON reader or writer. */
final class JsonScope {
private JsonScope() {
}
/** An array with no elements requires no separators or newlines before it is closed. */
static final int EMPTY_ARRAY = 1;

View file

@ -18,7 +18,6 @@ package com.squareup.moshi;
import java.io.EOFException;
import java.io.IOException;
import java.math.BigDecimal;
import javax.annotation.Nullable;
import okio.Buffer;
import okio.BufferedSource;
import okio.ByteString;
@ -31,7 +30,6 @@ final class JsonUtf8Reader extends JsonReader {
private static final ByteString UNQUOTED_STRING_TERMINALS
= ByteString.encodeUtf8("{}[]:, \n\t\r\f/\\;#=");
private static final ByteString LINEFEED_OR_CARRIAGE_RETURN = ByteString.encodeUtf8("\n\r");
private static final ByteString CLOSING_BLOCK_COMMENT = ByteString.encodeUtf8("*/");
private static final int PEEKED_NONE = 0;
private static final int PEEKED_BEGIN_OBJECT = 1;
@ -78,7 +76,8 @@ final class JsonUtf8Reader extends JsonReader {
private long peekedLong;
/**
* The number of characters in a peeked number literal.
* The number of characters in a peeked number literal. Increment 'pos' by
* this after reading a number.
*/
private int peekedNumberLength;
@ -87,38 +86,17 @@ final class JsonUtf8Reader extends JsonReader {
* This is populated before a numeric value is parsed and used if that parsing
* fails.
*/
private @Nullable String peekedString;
private String peekedString;
JsonUtf8Reader(BufferedSource source) {
if (source == null) {
throw new NullPointerException("source == null");
}
this.source = source;
this.buffer = source.getBuffer();
this.buffer = source.buffer();
pushScope(JsonScope.EMPTY_DOCUMENT);
}
/** Copy-constructor makes a deep copy for peeking. */
JsonUtf8Reader(JsonUtf8Reader copyFrom) {
super(copyFrom);
BufferedSource sourcePeek = copyFrom.source.peek();
this.source = sourcePeek;
this.buffer = sourcePeek.getBuffer();
this.peeked = copyFrom.peeked;
this.peekedLong = copyFrom.peekedLong;
this.peekedNumberLength = copyFrom.peekedNumberLength;
this.peekedString = copyFrom.peekedString;
// Make sure our buffer has as many bytes as the source's buffer. This is necessary because
// JsonUtf8Reader assumes any data it has peeked (like the peekedNumberLength) are buffered.
try {
sourcePeek.require(copyFrom.buffer.size());
} catch (IOException e) {
throw new AssertionError();
}
}
@Override public void beginArray() throws IOException {
int p = peeked;
if (p == PEEKED_NONE) {
@ -184,7 +162,7 @@ final class JsonUtf8Reader extends JsonReader {
if (p == PEEKED_NONE) {
p = doPeek();
}
return p != PEEKED_END_OBJECT && p != PEEKED_END_ARRAY && p != PEEKED_EOF;
return p != PEEKED_END_OBJECT && p != PEEKED_END_ARRAY;
}
@Override public Token peek() throws IOException {
@ -486,8 +464,7 @@ final class JsonUtf8Reader extends JsonReader {
}
// We've read a complete number. Decide if it's a PEEKED_LONG or a PEEKED_NUMBER.
if (last == NUMBER_CHAR_DIGIT && fitsInLong && (value != Long.MIN_VALUE || negative)
&& (value != 0 || !negative)) {
if (last == NUMBER_CHAR_DIGIT && fitsInLong && (value != Long.MIN_VALUE || negative)) {
peekedLong = negative ? value : -value;
buffer.skip(i);
return peeked = PEEKED_LONG;
@ -584,29 +561,8 @@ final class JsonUtf8Reader extends JsonReader {
return result;
}
@Override public void skipName() throws IOException {
if (failOnUnknown) {
throw new JsonDataException("Cannot skip unexpected " + peek() + " at " + getPath());
}
int p = peeked;
if (p == PEEKED_NONE) {
p = doPeek();
}
if (p == PEEKED_UNQUOTED_NAME) {
skipUnquotedValue();
} else if (p == PEEKED_DOUBLE_QUOTED_NAME) {
skipQuotedValue(DOUBLE_QUOTE_OR_SLASH);
} else if (p == PEEKED_SINGLE_QUOTED_NAME) {
skipQuotedValue(SINGLE_QUOTE_OR_SLASH);
} else if (p != PEEKED_BUFFERED_NAME) {
throw new JsonDataException("Expected a name but was " + peek() + " at path " + getPath());
}
peeked = PEEKED_NONE;
pathNames[stackSize - 1] = "null";
}
/**
* If {@code name} is in {@code options} this consumes it and returns its index.
* If {@code name} is in {@code options} this consumes it and returns it's index.
* Otherwise this returns -1 and no name is consumed.
*/
private int findName(String name, Options options) {
@ -681,7 +637,7 @@ final class JsonUtf8Reader extends JsonReader {
}
/**
* If {@code string} is in {@code options} this consumes it and returns its index.
* If {@code string} is in {@code options} this consumes it and returns it's index.
* Otherwise this returns -1 and no string is consumed.
*/
private int findString(String string, Options options) {
@ -713,7 +669,7 @@ final class JsonUtf8Reader extends JsonReader {
throw new JsonDataException("Expected a boolean but was " + peek() + " at path " + getPath());
}
@Override public @Nullable <T> T nextNull() throws IOException {
@Override public <T> T nextNull() throws IOException {
int p = peeked;
if (p == PEEKED_NONE) {
p = doPeek();
@ -793,7 +749,7 @@ final class JsonUtf8Reader extends JsonReader {
pathIndices[stackSize - 1]++;
return result;
} catch (NumberFormatException ignored) {
// Fall back to parse as a BigDecimal below.
// Fall back to parse as a double below.
}
} else if (p != PEEKED_BUFFERED) {
throw new JsonDataException("Expected a long but was " + peek()
@ -957,19 +913,11 @@ final class JsonUtf8Reader extends JsonReader {
pushScope(JsonScope.EMPTY_OBJECT);
count++;
} else if (p == PEEKED_END_ARRAY) {
count--;
if (count < 0) {
throw new JsonDataException(
"Expected a value but was " + peek() + " at path " + getPath());
}
stackSize--;
count--;
} else if (p == PEEKED_END_OBJECT) {
count--;
if (count < 0) {
throw new JsonDataException(
"Expected a value but was " + peek() + " at path " + getPath());
}
stackSize--;
count--;
} else if (p == PEEKED_UNQUOTED_NAME || p == PEEKED_UNQUOTED) {
skipUnquotedValue();
} else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_DOUBLE_QUOTED_NAME) {
@ -978,9 +926,6 @@ final class JsonUtf8Reader extends JsonReader {
skipQuotedValue(SINGLE_QUOTE_OR_SLASH);
} else if (p == PEEKED_NUMBER) {
buffer.skip(peekedNumberLength);
} else if (p == PEEKED_EOF) {
throw new JsonDataException(
"Expected a value but was " + peek() + " at path " + getPath());
}
peeked = PEEKED_NONE;
} while (count != 0);
@ -992,7 +937,8 @@ final class JsonUtf8Reader extends JsonReader {
/**
* Returns the next character in the stream that is neither whitespace nor a
* part of a comment. When this returns, the returned character is always at
* {@code buffer.getByte(0)}.
* {@code buffer[pos-1]}; this means the caller can always push back the
* returned character by decrementing {@code pos}.
*/
private int nextNonWhitespace(boolean throwOnEof) throws IOException {
/*
@ -1023,9 +969,11 @@ final class JsonUtf8Reader extends JsonReader {
// skip a /* c-style comment */
buffer.readByte(); // '/'
buffer.readByte(); // '*'
if (!skipToEndOfBlockComment()) {
if (!skipTo("*/")) {
throw syntaxError("Unterminated comment");
}
buffer.readByte(); // '*'
buffer.readByte(); // '/'
p = 0;
continue;
@ -1074,17 +1022,20 @@ final class JsonUtf8Reader extends JsonReader {
}
/**
* Skips through the next closing block comment.
* @param toFind a string to search for. Must not contain a newline.
*/
private boolean skipToEndOfBlockComment() throws IOException {
long index = source.indexOf(CLOSING_BLOCK_COMMENT);
boolean found = index != -1;
buffer.skip(found ? index + CLOSING_BLOCK_COMMENT.size() : buffer.size());
return found;
}
@Override public JsonReader peekJson() {
return new JsonUtf8Reader(this);
private boolean skipTo(String toFind) throws IOException {
outer:
for (; source.request(toFind.length()); ) {
for (int c = 0; c < toFind.length(); c++) {
if (buffer.getByte(c) != toFind.charAt(c)) {
buffer.readByte();
continue outer;
}
}
return true;
}
return false;
}
@Override public String toString() {

View file

@ -16,9 +16,7 @@
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;
@ -78,12 +76,8 @@ final class JsonUtf8Writer extends JsonWriter {
}
@Override public JsonWriter beginArray() throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"Array cannot be used as a map key in JSON at path " + getPath());
}
writeDeferredName();
return open(EMPTY_ARRAY, NONEMPTY_ARRAY, "[");
return open(EMPTY_ARRAY, "[");
}
@Override public JsonWriter endArray() throws IOException {
@ -91,12 +85,8 @@ final class JsonUtf8Writer extends JsonWriter {
}
@Override public JsonWriter beginObject() throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"Object cannot be used as a map key in JSON at path " + getPath());
}
writeDeferredName();
return open(EMPTY_OBJECT, NONEMPTY_OBJECT, "{");
return open(EMPTY_OBJECT, "{");
}
@Override public JsonWriter endObject() throws IOException {
@ -108,15 +98,8 @@ final class JsonUtf8Writer extends JsonWriter {
* 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;
}
private JsonWriter open(int empty, String openBracket) throws IOException {
beforeValue();
checkStack();
pushScope(empty);
pathIndices[stackSize - 1] = 0;
sink.writeUtf8(openBracket);
@ -135,11 +118,6 @@ final class JsonUtf8Writer extends JsonWriter {
if (deferredName != null) {
throw new IllegalStateException("Dangling name: " + deferredName);
}
if (stackSize == ~flattenStackSize) {
// Cancel this close. Restore the flattenStackSize so we're ready to flatten again!
flattenStackSize = ~flattenStackSize;
return this;
}
stackSize--;
pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected!
@ -158,8 +136,7 @@ final class JsonUtf8Writer extends JsonWriter {
if (stackSize == 0) {
throw new IllegalStateException("JsonWriter is closed.");
}
int context = peekScope();
if ((context != EMPTY_OBJECT && context != NONEMPTY_OBJECT) || deferredName != null) {
if (deferredName != null) {
throw new IllegalStateException("Nesting problem.");
}
deferredName = name;
@ -191,10 +168,6 @@ final class JsonUtf8Writer extends JsonWriter {
}
@Override public JsonWriter nullValue() throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"null cannot be used as a map key in JSON at path " + getPath());
}
if (deferredName != null) {
if (serializeNulls) {
writeDeferredName();
@ -210,10 +183,6 @@ final class JsonUtf8Writer extends JsonWriter {
}
@Override public JsonWriter value(boolean value) throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"Boolean cannot be used as a map key in JSON at path " + getPath());
}
writeDeferredName();
beforeValue();
sink.writeUtf8(value ? "true" : "false");
@ -253,7 +222,7 @@ final class JsonUtf8Writer extends JsonWriter {
return this;
}
@Override public JsonWriter value(@Nullable Number value) throws IOException {
@Override public JsonWriter value(Number value) throws IOException {
if (value == null) {
return nullValue();
}
@ -273,18 +242,6 @@ final class JsonUtf8Writer extends JsonWriter {
return this;
}
@Override public JsonWriter value(BufferedSource source) throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"BufferedSource cannot be used as a map key in JSON at path " + getPath());
}
writeDeferredName();
beforeValue();
sink.writeAll(source);
pathIndices[stackSize - 1]++;
return this;
}
/**
* Ensures all buffered data is written to the underlying {@link Sink}
* and flushes that writer.

View file

@ -20,10 +20,8 @@ import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import javax.annotation.Nullable;
import static com.squareup.moshi.JsonScope.CLOSED;
/**
* This class reads a JSON document by traversing a Java object comprising maps, lists, and JSON
@ -33,11 +31,11 @@ import static com.squareup.moshi.JsonScope.CLOSED;
* <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
* list with a {@link ListIterator}. 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.
* replaces the map with an {@link Iterator} 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
@ -50,31 +48,17 @@ 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;
private final Object[] stack = new Object[32];
JsonValueReader(Object root) {
public 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);
ListIterator<?> iterator = peeked.listIterator();
stack[stackSize - 1] = iterator;
scopes[stackSize - 1] = JsonScope.EMPTY_ARRAY;
pathIndices[stackSize - 1] = 0;
@ -86,8 +70,8 @@ final class JsonValueReader extends JsonReader {
}
@Override public void endArray() throws IOException {
JsonIterator peeked = require(JsonIterator.class, Token.END_ARRAY);
if (peeked.endToken != Token.END_ARRAY || peeked.hasNext()) {
ListIterator<?> peeked = require(ListIterator.class, Token.END_ARRAY);
if (peeked.hasNext()) {
throw typeMismatch(peeked, Token.END_ARRAY);
}
remove();
@ -96,8 +80,7 @@ final class JsonValueReader extends JsonReader {
@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);
Iterator<?> iterator = peeked.entrySet().iterator();
stack[stackSize - 1] = iterator;
scopes[stackSize - 1] = JsonScope.EMPTY_OBJECT;
@ -108,8 +91,8 @@ final class JsonValueReader extends JsonReader {
}
@Override public void endObject() throws IOException {
JsonIterator peeked = require(JsonIterator.class, Token.END_OBJECT);
if (peeked.endToken != Token.END_OBJECT || peeked.hasNext()) {
Iterator<?> peeked = require(Iterator.class, Token.END_OBJECT);
if (peeked instanceof ListIterator || peeked.hasNext()) {
throw typeMismatch(peeked, Token.END_OBJECT);
}
pathNames[stackSize - 1] = null;
@ -117,7 +100,8 @@ final class JsonValueReader extends JsonReader {
}
@Override public boolean hasNext() throws IOException {
if (stackSize == 0) return false;
// TODO(jwilson): this is consistent with BufferedSourceJsonReader but it doesn't make sense.
if (stackSize == 0) return true;
Object peeked = stack[stackSize - 1];
return !(peeked instanceof Iterator) || ((Iterator) peeked).hasNext();
@ -128,7 +112,8 @@ final class JsonValueReader extends JsonReader {
// If the top of the stack is an iterator, take its first element and push it on the stack.
Object peeked = stack[stackSize - 1];
if (peeked instanceof JsonIterator) return ((JsonIterator) peeked).endToken;
if (peeked instanceof ListIterator) return Token.END_ARRAY;
if (peeked instanceof Iterator) return Token.END_OBJECT;
if (peeked instanceof List) return Token.BEGIN_ARRAY;
if (peeked instanceof Map) return Token.BEGIN_OBJECT;
if (peeked instanceof Map.Entry) return Token.NAME;
@ -165,47 +150,16 @@ final class JsonValueReader extends JsonReader {
return -1;
}
@Override public void skipName() throws IOException {
if (failOnUnknown) {
throw new JsonDataException("Cannot skip unexpected " + peek() + " at " + getPath());
}
Map.Entry<?, ?> peeked = require(Map.Entry.class, Token.NAME);
// Swap the Map.Entry for its value on the stack.
stack[stackSize - 1] = peeked.getValue();
pathNames[stackSize - 2] = "null";
}
@Override public String nextString() throws IOException {
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);
String peeked = require(String.class, Token.STRING);
remove();
return peeked;
}
@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;
String peeked = require(String.class, Token.STRING);
for (int i = 0, length = options.strings.length; i < length; i++) {
if (options.strings[i].equals(peekedString)) {
if (options.strings[i].equals(peeked)) {
remove();
return i;
}
@ -219,7 +173,7 @@ final class JsonValueReader extends JsonReader {
return peeked;
}
@Override public @Nullable <T> T nextNull() throws IOException {
@Override public <T> T nextNull() throws IOException {
require(Void.class, Token.NULL);
remove();
return null;
@ -308,9 +262,6 @@ final class JsonValueReader extends JsonReader {
Object skipped = stackSize != 0 ? stack[stackSize - 1] : null;
if (skipped instanceof JsonIterator) {
throw new JsonDataException("Expected a value but was " + peek() + " at path " + getPath());
}
if (skipped instanceof Map.Entry) {
// We're skipping a name. Promote the map entry's value.
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) stack[stackSize - 1];
@ -318,15 +269,9 @@ final class JsonValueReader extends JsonReader {
} else if (stackSize > 0) {
// We're skipping a value.
remove();
} else {
throw new JsonDataException("Expected a value but was " + peek() + " at path " + getPath());
}
}
@Override public JsonReader peekJson() {
return new JsonValueReader(this);
}
@Override void promoteNameToValue() throws IOException {
if (hasNext()) {
String name = nextName();
@ -337,19 +282,13 @@ final class JsonValueReader extends JsonReader {
@Override public void close() throws IOException {
Arrays.fill(stack, 0, stackSize, null);
stack[0] = JSON_READER_CLOSED;
scopes[0] = CLOSED;
scopes[0] = JsonScope.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);
throw new JsonDataException("Nesting too deep at " + getPath());
}
stack[stackSize++] = newTop;
}
@ -358,7 +297,7 @@ final class JsonValueReader extends JsonReader {
* Returns the top of the stack which is required to be a {@code type}. Throws if this reader is
* closed, or if the type isn't what was expected.
*/
private @Nullable <T> T require(Class<T> type, Token expected) throws IOException {
private <T> T require(Class<T> type, Token expected) throws IOException {
Object peeked = (stackSize != 0 ? stack[stackSize - 1] : null);
if (type.isInstance(peeked)) {
@ -398,33 +337,4 @@ final class JsonValueReader extends JsonReader {
}
}
}
static final class JsonIterator implements Iterator<Object>, Cloneable {
final Token endToken;
final Object[] array;
int next;
JsonIterator(Token endToken, Object[] array, int next) {
this.endToken = endToken;
this.array = array;
this.next = next;
}
@Override public boolean hasNext() {
return next < array.length;
}
@Override public Object next() {
return array[next++];
}
@Override public void remove() {
throw new UnsupportedOperationException();
}
@Override protected JsonIterator clone() {
// No need to copy the array; it's read-only.
return new JsonIterator(endToken, array, next);
}
}
}

View file

@ -20,8 +20,6 @@ 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;
@ -32,8 +30,8 @@ import static java.lang.Double.POSITIVE_INFINITY;
/** Writes JSON by building a Java object comprising maps, lists, and JSON primitives. */
final class JsonValueWriter extends JsonWriter {
Object[] stack = new Object[32];
private @Nullable String deferredName;
private final Object[] stack = new Object[32];
private String deferredName;
JsonValueWriter() {
pushScope(EMPTY_DOCUMENT);
@ -48,16 +46,9 @@ final class JsonValueWriter extends JsonWriter {
}
@Override public JsonWriter beginArray() throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"Array cannot be used as a map key in JSON at path " + getPath());
if (stackSize == stack.length) {
throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?");
}
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;
@ -70,11 +61,6 @@ final class JsonValueWriter extends JsonWriter {
if (peekScope() != EMPTY_ARRAY) {
throw new IllegalStateException("Nesting problem.");
}
if (stackSize == ~flattenStackSize) {
// Cancel this close. Restore the flattenStackSize so we're ready to flatten again!
flattenStackSize = ~flattenStackSize;
return this;
}
stackSize--;
stack[stackSize] = null;
pathIndices[stackSize - 1]++;
@ -82,16 +68,9 @@ final class JsonValueWriter extends JsonWriter {
}
@Override public JsonWriter beginObject() throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"Object cannot be used as a map key in JSON at path " + getPath());
if (stackSize == stack.length) {
throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?");
}
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;
@ -100,17 +79,9 @@ final class JsonValueWriter extends JsonWriter {
}
@Override public JsonWriter endObject() throws IOException {
if (peekScope() != EMPTY_OBJECT) {
if (peekScope() != EMPTY_OBJECT || deferredName != null) {
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;
@ -135,7 +106,7 @@ final class JsonValueWriter extends JsonWriter {
return this;
}
@Override public JsonWriter value(@Nullable String value) throws IOException {
@Override public JsonWriter value(String value) throws IOException {
if (promoteValueToName) {
return name(value);
}
@ -145,30 +116,18 @@ final class JsonValueWriter extends JsonWriter {
}
@Override public JsonWriter nullValue() throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"null cannot be used as a map key in JSON at path " + getPath());
}
add(null);
pathIndices[stackSize - 1]++;
return this;
}
@Override public JsonWriter value(boolean value) throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"Boolean cannot be used as a map key in JSON at path " + getPath());
}
add(value);
pathIndices[stackSize - 1]++;
return this;
}
@Override public JsonWriter value(@Nullable Boolean value) throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"Boolean cannot be used as a map key in JSON at path " + getPath());
}
@Override public JsonWriter value(Boolean value) throws IOException {
add(value);
pathIndices[stackSize - 1]++;
return this;
@ -196,7 +155,7 @@ final class JsonValueWriter extends JsonWriter {
return this;
}
@Override public JsonWriter value(@Nullable Number value) throws IOException {
@Override public JsonWriter value(Number value) throws IOException {
// If it's trivially converted to a long, do that.
if (value instanceof Byte
|| value instanceof Short
@ -210,10 +169,6 @@ final class JsonValueWriter extends JsonWriter {
return value(value.doubleValue());
}
if (value == null) {
return nullValue();
}
// Everything else gets converted to a BigDecimal.
BigDecimal bigDecimalValue = value instanceof BigDecimal
? ((BigDecimal) value)
@ -226,23 +181,6 @@ final class JsonValueWriter extends JsonWriter {
return this;
}
@Override public JsonWriter value(BufferedSource source) throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"BufferedSource cannot be used as a map key in JSON at path " + getPath());
}
Object value = JsonReader.of(source).readJsonValue();
boolean serializeNulls = this.serializeNulls;
this.serializeNulls = true;
try {
add(value);
} finally {
this.serializeNulls = serializeNulls;
}
pathIndices[stackSize - 1]++;
return this;
}
@Override public void close() throws IOException {
int size = stackSize;
if (size > 1 || size == 1 && scopes[size - 1] != NONEMPTY_DOCUMENT) {
@ -257,7 +195,7 @@ final class JsonValueWriter extends JsonWriter {
}
}
private JsonValueWriter add(@Nullable Object newTop) {
private JsonValueWriter add(Object newTop) {
int scope = peekScope();
if (stackSize == 1) {

View file

@ -18,15 +18,9 @@ package com.squareup.moshi;
import java.io.Closeable;
import java.io.Flushable;
import java.io.IOException;
import java.util.Arrays;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import okio.BufferedSink;
import okio.BufferedSource;
import static com.squareup.moshi.JsonScope.EMPTY_ARRAY;
import static com.squareup.moshi.JsonScope.EMPTY_OBJECT;
import static com.squareup.moshi.JsonScope.NONEMPTY_ARRAY;
import static com.squareup.moshi.JsonScope.NONEMPTY_OBJECT;
/**
@ -125,13 +119,13 @@ import static com.squareup.moshi.JsonScope.NONEMPTY_OBJECT;
* malformed JSON string will fail with an {@link IllegalStateException}.
*/
public abstract class JsonWriter implements Closeable, Flushable {
// The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack will
// grow itself up to 256 levels of nesting including the top-level document. Deeper nesting is
// prone to trigger StackOverflowErrors.
// The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack permits
// up to 32 levels of nesting including the top-level document. Deeper nesting is prone to trigger
// StackOverflowErrors.
int stackSize = 0;
int[] scopes = new int[32];
String[] pathNames = new String[32];
int[] pathIndices = new int[32];
final int[] scopes = new int[32];
final String[] pathNames = new String[32];
final int[] pathIndices = new int[32];
/**
* A string containing a full set of spaces for a single level of indentation, or null for no
@ -142,28 +136,8 @@ public abstract class JsonWriter implements Closeable, Flushable {
boolean serializeNulls;
boolean promoteValueToName;
/**
* Controls the deepest stack size that has begin/end pairs flattened:
*
* <ul>
* <li>If -1, no begin/end pairs are being suppressed.
* <li>If positive, this is the deepest stack size whose begin/end pairs are eligible to be
* flattened.
* <li>If negative, it is the bitwise inverse (~) of the deepest stack size whose begin/end
* pairs have been flattened.
* </ul>
*
* <p>We differentiate between what layer would be flattened (positive) from what layer is being
* flattened (negative) so that we don't double-flatten.
*
* <p>To accommodate nested flattening we require callers to track the previous state when they
* provide a new state. The previous state is returned from {@link #beginFlatten} and restored
* with {@link #endFlatten}.
*/
int flattenStackSize = -1;
/** Returns a new instance that writes UTF-8 encoded JSON to {@code sink}. */
@CheckReturnValue public static JsonWriter of(BufferedSink sink) {
public static JsonWriter of(BufferedSink sink) {
return new JsonUtf8Writer(sink);
}
@ -179,26 +153,10 @@ public abstract class JsonWriter implements Closeable, Flushable {
return scopes[stackSize - 1];
}
/** Before pushing a value on the stack this confirms that the stack has capacity. */
final boolean checkStack() {
if (stackSize != scopes.length) return false;
if (stackSize == 256) {
final void pushScope(int newTop) {
if (stackSize == scopes.length) {
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;
}
@ -223,7 +181,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
* Returns a string containing only whitespace, used for each level of
* indentation. If empty, the encoded document will be compact.
*/
@CheckReturnValue public final String getIndent() {
public final String getIndent() {
return indent != null ? indent : "";
}
@ -246,7 +204,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
/**
* Returns true if this writer has relaxed syntax rules.
*/
@CheckReturnValue public final boolean isLenient() {
public final boolean isLenient() {
return lenient;
}
@ -262,7 +220,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
* Returns true if object members are serialized when their value is null.
* This has no impact on array elements. The default is false.
*/
@CheckReturnValue public final boolean getSerializeNulls() {
public final boolean getSerializeNulls() {
return serializeNulls;
}
@ -299,7 +257,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
/**
* Encodes the property name.
*
* @param name the name of the forthcoming value. Must not be null.
* @param name the name of the forthcoming value. May not be null.
* @return this writer.
*/
public abstract JsonWriter name(String name) throws IOException;
@ -310,7 +268,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
* @param value the literal string value, or null to encode a null literal.
* @return this writer.
*/
public abstract JsonWriter value(@Nullable String value) throws IOException;
public abstract JsonWriter value(String value) throws IOException;
/**
* Encodes {@code null}.
@ -331,7 +289,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
*
* @return this writer.
*/
public abstract JsonWriter value(@Nullable Boolean value) throws IOException;
public abstract JsonWriter value(Boolean value) throws IOException;
/**
* Encodes {@code value}.
@ -356,16 +314,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
* {@linkplain Double#isInfinite() infinities}.
* @return this writer.
*/
public abstract JsonWriter value(@Nullable Number value) throws IOException;
/**
* Writes {@code source} directly without encoding its contents.
* Since no validation is performed, {@link #setSerializeNulls} and other writer configurations
* are not respected.
*
* @return this writer.
*/
public abstract JsonWriter value(BufferedSource source) throws IOException;
public abstract JsonWriter value(Number value) throws IOException;
/**
* Changes the writer to treat the next value as a string name. This is useful for map adapters so
@ -379,93 +328,11 @@ public abstract class JsonWriter implements Closeable, Flushable {
promoteValueToName = true;
}
/**
* Cancels immediately-nested calls to {@link #beginArray()} or {@link #beginObject()} and their
* matching calls to {@link #endArray} or {@link #endObject()}. Use this to compose JSON adapters
* without nesting.
*
* <p>For example, the following creates JSON with nested arrays: {@code [1,[2,3,4],5]}.
*
* <pre>{@code
*
* JsonAdapter<List<Integer>> integersAdapter = ...
*
* public void writeNumbers(JsonWriter writer) {
* writer.beginArray();
* writer.value(1);
* integersAdapter.toJson(writer, Arrays.asList(2, 3, 4));
* writer.value(5);
* writer.endArray();
* }
* }</pre>
*
* <p>With flattening we can create JSON with a single array {@code [1,2,3,4,5]}:
*
* <pre>{@code
*
* JsonAdapter<List<Integer>> integersAdapter = ...
*
* public void writeNumbers(JsonWriter writer) {
* writer.beginArray();
* int token = writer.beginFlatten();
* writer.value(1);
* integersAdapter.toJson(writer, Arrays.asList(2, 3, 4));
* writer.value(5);
* writer.endFlatten(token);
* writer.endArray();
* }
* }</pre>
*
* <p>This method flattens arrays within arrays:
*
* <pre>{@code
*
* Emit: [1, [2, 3, 4], 5]
* To produce: [1, 2, 3, 4, 5]
* }</pre>
*
* It also flattens objects within objects. Do not call {@link #name} before writing a flattened
* object.
*
* <pre>{@code
*
* Emit: {"a": 1, {"b": 2}, "c": 3}
* To Produce: {"a": 1, "b": 2, "c": 3}
* }</pre>
*
* Other combinations are permitted but do not perform flattening. For example, objects inside of
* arrays are not flattened:
*
* <pre>{@code
*
* Emit: [1, {"b": 2}, 3, [4, 5], 6]
* To Produce: [1, {"b": 2}, 3, 4, 5, 6]
* }</pre>
*
* <p>This method returns an opaque token. Callers must match all calls to this method with a call
* to {@link #endFlatten} with the matching token.
*/
@CheckReturnValue public final int beginFlatten() {
int context = peekScope();
if (context != NONEMPTY_OBJECT && context != EMPTY_OBJECT
&& context != NONEMPTY_ARRAY && context != EMPTY_ARRAY) {
throw new IllegalStateException("Nesting problem.");
}
int token = flattenStackSize;
flattenStackSize = stackSize;
return token;
}
/** Ends nested call flattening created by {@link #beginFlatten}. */
public final void endFlatten(int token) {
flattenStackSize = token;
}
/**
* Returns a <a href="http://goessner.net/articles/JsonPath/">JsonPath</a> to
* the current location in the JSON value.
*/
@CheckReturnValue public final String getPath() {
public final String getPath() {
return JsonScope.getPath(stackSize, scopes, 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
* comparable and non-null.
*/
LinkedHashTreeMap() {
public LinkedHashTreeMap() {
this(null);
}
@ -66,10 +66,8 @@ final class LinkedHashTreeMap<K, V> extends AbstractMap<K, V> implements Seriali
* @param comparator the comparator to order elements with, or {@code null} to
* use the natural ordering.
*/
@SuppressWarnings({
"unchecked", "rawtypes" // Unsafe! if comparator is null, this assumes K is comparable.
})
LinkedHashTreeMap(Comparator<? super K> comparator) {
@SuppressWarnings({ "unchecked", "rawtypes" }) // unsafe! if comparator is null, this assumes K is comparable
public LinkedHashTreeMap(Comparator<? super K> comparator) {
this.comparator = comparator != null
? comparator
: (Comparator) NATURAL_ORDER;
@ -475,14 +473,14 @@ final class LinkedHashTreeMap<K, V> extends AbstractMap<K, V> implements Seriali
V value;
int height;
/** Create the header entry. */
/** Create the header entry */
Node() {
key = null;
hash = -1;
next = prev = this;
}
/** Create a regular entry. */
/** Create a regular entry */
Node(Node<K, V> parent, K key, int hash, Node<K, V> next, Node<K, V> prev) {
this.parent = parent;
this.key = key;
@ -667,7 +665,7 @@ final class LinkedHashTreeMap<K, V> extends AbstractMap<K, V> implements Seriali
* comparisons. Using this class to create a tree of size <i>S</i> is
* {@code O(S)}.
*/
static final class AvlBuilder<K, V> {
final static class AvlBuilder<K, V> {
/** This stack is a singly linked list, linked by the 'parent' field. */
private Node<K, V> stack;
private int leavesToSkip;

View file

@ -20,7 +20,6 @@ import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
/**
* Converts maps with string keys to JSON objects.
@ -29,7 +28,7 @@ import javax.annotation.Nullable;
*/
final class MapJsonAdapter<K, V> extends JsonAdapter<Map<K, V>> {
public static final Factory FACTORY = new Factory() {
@Override public @Nullable JsonAdapter<?> create(
@Override public JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
if (!annotations.isEmpty()) return null;
Class<?> rawType = Types.getRawType(type);
@ -42,7 +41,7 @@ final class MapJsonAdapter<K, V> extends JsonAdapter<Map<K, V>> {
private final JsonAdapter<K> keyAdapter;
private final JsonAdapter<V> valueAdapter;
MapJsonAdapter(Moshi moshi, Type keyType, Type valueType) {
public MapJsonAdapter(Moshi moshi, Type keyType, Type valueType) {
this.keyAdapter = moshi.adapter(keyType);
this.valueAdapter = moshi.adapter(valueType);
}

View file

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

View file

@ -15,23 +15,15 @@
*/
package com.squareup.moshi;
import com.squareup.moshi.internal.Util;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import static com.squareup.moshi.internal.Util.generatedAdapter;
final class StandardJsonAdapters {
private StandardJsonAdapters() {
}
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
@Override public JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
@ -56,12 +48,6 @@ final class StandardJsonAdapters {
if (type == Object.class) return new ObjectJsonAdapter(moshi).nullSafe();
Class<?> rawType = Types.getRawType(type);
@Nullable JsonAdapter<?> generatedAdapter = generatedAdapter(moshi, type, rawType);
if (generatedAdapter != null) {
return generatedAdapter;
}
if (rawType.isEnum()) {
//noinspection unchecked
return new EnumJsonAdapter<>((Class<? extends Enum>) rawType).nullSafe();
@ -230,7 +216,7 @@ final class StandardJsonAdapters {
private final T[] constants;
private final JsonReader.Options options;
EnumJsonAdapter(Class<T> enumType) {
public EnumJsonAdapter(Class<T> enumType) {
this.enumType = enumType;
try {
constants = enumType.getEnumConstants();
@ -252,10 +238,10 @@ final class StandardJsonAdapters {
if (index != -1) return constants[index];
// We can consume the string safely, we are terminating anyway.
String path = reader.getPath();
String name = reader.nextString();
throw new JsonDataException("Expected one of "
+ Arrays.asList(nameStrings) + " but was " + name + " at path " + path);
+ Arrays.asList(nameStrings) + " but was " + name + " at path "
+ reader.getPath());
}
@Override public void toJson(JsonWriter writer, T value) throws IOException {
@ -277,45 +263,13 @@ final class StandardJsonAdapters {
*/
static final class ObjectJsonAdapter extends JsonAdapter<Object> {
private final Moshi moshi;
private final JsonAdapter<List> listJsonAdapter;
private final JsonAdapter<Map> mapAdapter;
private final JsonAdapter<String> stringAdapter;
private final JsonAdapter<Double> doubleAdapter;
private final JsonAdapter<Boolean> booleanAdapter;
ObjectJsonAdapter(Moshi moshi) {
public ObjectJsonAdapter(Moshi moshi) {
this.moshi = moshi;
this.listJsonAdapter = moshi.adapter(List.class);
this.mapAdapter = moshi.adapter(Map.class);
this.stringAdapter = moshi.adapter(String.class);
this.doubleAdapter = moshi.adapter(Double.class);
this.booleanAdapter = moshi.adapter(Boolean.class);
}
@Override public Object fromJson(JsonReader reader) throws IOException {
switch (reader.peek()) {
case BEGIN_ARRAY:
return listJsonAdapter.fromJson(reader);
case BEGIN_OBJECT:
return mapAdapter.fromJson(reader);
case STRING:
return stringAdapter.fromJson(reader);
case NUMBER:
return doubleAdapter.fromJson(reader);
case BOOLEAN:
return booleanAdapter.fromJson(reader);
case NULL:
return reader.nextNull();
default:
throw new IllegalStateException(
"Expected a value but was " + reader.peek() + " at path " + reader.getPath());
}
return reader.readJsonValue();
}
@Override public void toJson(JsonWriter writer, Object value) throws IOException {

View file

@ -15,13 +15,10 @@
*/
package com.squareup.moshi;
import com.squareup.moshi.internal.Util.GenericArrayTypeImpl;
import com.squareup.moshi.internal.Util.ParameterizedTypeImpl;
import com.squareup.moshi.internal.Util.WildcardTypeImpl;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.GenericDeclaration;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
@ -34,59 +31,23 @@ import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.Set;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import static com.squareup.moshi.internal.Util.EMPTY_TYPE_ARRAY;
import static com.squareup.moshi.internal.Util.getGenericSupertype;
import static com.squareup.moshi.internal.Util.resolve;
/** Factory methods for types. */
@CheckReturnValue
public final class Types {
static final Type[] EMPTY_TYPE_ARRAY = new Type[] {};
private Types() {
}
/**
* Resolves the generated {@link JsonAdapter} fully qualified class name for a given
* {@link JsonClass JsonClass-annotated} {@code clazz}. This is the same lookup logic used by
* both the Moshi code generation as well as lookup for any JsonClass-annotated classes. This can
* be useful if generating your own JsonAdapters without using Moshi's first party code gen.
*
* @param clazz the class to calculate a generated JsonAdapter name for.
* @return the resolved fully qualified class name to the expected generated JsonAdapter class.
* Note that this name will always be a top-level class name and not a nested class.
*/
public static String generatedJsonAdapterName(Class<?> clazz) {
if (clazz.getAnnotation(JsonClass.class) == null) {
throw new IllegalArgumentException("Class does not have a JsonClass annotation: " + clazz);
}
return generatedJsonAdapterName(clazz.getName());
}
/**
* Resolves the generated {@link JsonAdapter} fully qualified class name for a given
* {@link JsonClass JsonClass-annotated} {@code className}. This is the same lookup logic used by
* both the Moshi code generation as well as lookup for any JsonClass-annotated classes. This can
* be useful if generating your own JsonAdapters without using Moshi's first party code gen.
*
* @param className the fully qualified class to calculate a generated JsonAdapter name for.
* @return the resolved fully qualified class name to the expected generated JsonAdapter class.
* Note that this name will always be a top-level class name and not a nested class.
*/
public static String generatedJsonAdapterName(String className) {
return className.replace("$", "_") + "JsonAdapter";
}
/**
* Checks if {@code annotations} contains {@code jsonQualifier}.
* Returns the subset of {@code annotations} without {@code jsonQualifier}, or null if {@code
* annotations} does not contain {@code jsonQualifier}.
*/
public static @Nullable Set<? extends Annotation> nextAnnotations(
Set<? extends Annotation> annotations,
public static 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.");
@ -96,7 +57,7 @@ public final class Types {
}
for (Annotation annotation : annotations) {
if (jsonQualifier.equals(annotation.annotationType())) {
Set<? extends Annotation> delegateAnnotations = new LinkedHashSet<>(annotations);
Set<Annotation> delegateAnnotations = new LinkedHashSet<>(annotations);
delegateAnnotations.remove(annotation);
return Collections.unmodifiableSet(delegateAnnotations);
}
@ -144,6 +105,36 @@ public final class Types {
return new WildcardTypeImpl(new Type[] { Object.class }, new Type[] { bound });
}
/**
* Returns a type that is functionally equal but not necessarily equal according to {@link
* Object#equals(Object) Object.equals()}.
*/
static Type canonicalize(Type type) {
if (type instanceof Class) {
Class<?> c = (Class<?>) type;
return c.isArray() ? new GenericArrayTypeImpl(canonicalize(c.getComponentType())) : c;
} else if (type instanceof ParameterizedType) {
if (type instanceof ParameterizedTypeImpl) return type;
ParameterizedType p = (ParameterizedType) type;
return new ParameterizedTypeImpl(p.getOwnerType(),
p.getRawType(), p.getActualTypeArguments());
} else if (type instanceof GenericArrayType) {
if (type instanceof GenericArrayTypeImpl) return type;
GenericArrayType g = (GenericArrayType) type;
return new GenericArrayTypeImpl(g.getGenericComponentType());
} else if (type instanceof WildcardType) {
if (type instanceof WildcardTypeImpl) return type;
WildcardType w = (WildcardType) type;
return new WildcardTypeImpl(w.getUpperBounds(), w.getLowerBounds());
} else {
return type; // This type is unsupported!
}
}
public static Class<?> getRawType(Type type) {
if (type instanceof Class<?>) {
// type is a normal class.
@ -158,7 +149,7 @@ public final class Types {
return (Class<?>) rawType;
} else if (type instanceof GenericArrayType) {
Type componentType = ((GenericArrayType) type).getGenericComponentType();
Type componentType = ((GenericArrayType)type).getGenericComponentType();
return Array.newInstance(getRawType(componentType), 0).getClass();
} else if (type instanceof TypeVariable) {
@ -176,106 +167,6 @@ public final class Types {
}
}
/**
* Returns the element type of this collection type.
* @throws IllegalArgumentException if this type is not a collection.
*/
public static Type collectionElementType(Type context, Class<?> contextRawType) {
Type collectionType = getSupertype(context, contextRawType, Collection.class);
if (collectionType instanceof WildcardType) {
collectionType = ((WildcardType) collectionType).getUpperBounds()[0];
}
if (collectionType instanceof ParameterizedType) {
return ((ParameterizedType) collectionType).getActualTypeArguments()[0];
}
return Object.class;
}
/** Returns true if {@code a} and {@code b} are equal. */
public static boolean equals(@Nullable Type a, @Nullable Type b) {
if (a == b) {
return true; // Also handles (a == null && b == null).
} else if (a instanceof Class) {
if (b instanceof GenericArrayType) {
return equals(((Class) a).getComponentType(),
((GenericArrayType) b).getGenericComponentType());
}
return a.equals(b); // Class already specifies equals().
} else if (a instanceof ParameterizedType) {
if (!(b instanceof ParameterizedType)) return false;
ParameterizedType pa = (ParameterizedType) a;
ParameterizedType pb = (ParameterizedType) b;
Type[] aTypeArguments = pa instanceof ParameterizedTypeImpl
? ((ParameterizedTypeImpl) pa).typeArguments
: pa.getActualTypeArguments();
Type[] bTypeArguments = pb instanceof ParameterizedTypeImpl
? ((ParameterizedTypeImpl) pb).typeArguments
: pb.getActualTypeArguments();
return equals(pa.getOwnerType(), pb.getOwnerType())
&& pa.getRawType().equals(pb.getRawType())
&& Arrays.equals(aTypeArguments, bTypeArguments);
} else if (a instanceof GenericArrayType) {
if (b instanceof Class) {
return equals(((Class) b).getComponentType(),
((GenericArrayType) a).getGenericComponentType());
}
if (!(b instanceof GenericArrayType)) return false;
GenericArrayType ga = (GenericArrayType) a;
GenericArrayType gb = (GenericArrayType) b;
return equals(ga.getGenericComponentType(), gb.getGenericComponentType());
} else if (a instanceof WildcardType) {
if (!(b instanceof WildcardType)) return false;
WildcardType wa = (WildcardType) a;
WildcardType wb = (WildcardType) b;
return Arrays.equals(wa.getUpperBounds(), wb.getUpperBounds())
&& Arrays.equals(wa.getLowerBounds(), wb.getLowerBounds());
} else if (a instanceof TypeVariable) {
if (!(b instanceof TypeVariable)) return false;
TypeVariable<?> va = (TypeVariable<?>) a;
TypeVariable<?> vb = (TypeVariable<?>) b;
return va.getGenericDeclaration() == vb.getGenericDeclaration()
&& va.getName().equals(vb.getName());
} else {
// This isn't a supported type.
return false;
}
}
/**
* @param clazz the target class to read the {@code fieldName} field annotations from.
* @param fieldName the target field name on {@code clazz}.
* @return a set of {@link JsonQualifier}-annotated {@link Annotation} instances retrieved from
* the targeted field. Can be empty if none are found.
*/
public static Set<? extends Annotation> getFieldJsonQualifierAnnotations(Class<?> clazz,
String fieldName) {
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
Annotation[] fieldAnnotations = field.getDeclaredAnnotations();
Set<Annotation> annotations = new LinkedHashSet<>(fieldAnnotations.length);
for (Annotation annotation : fieldAnnotations) {
if (annotation.annotationType().isAnnotationPresent(JsonQualifier.class)) {
annotations.add(annotation);
}
}
return Collections.unmodifiableSet(annotations);
} catch (NoSuchFieldException e) {
throw new IllegalArgumentException("Could not access field "
+ fieldName
+ " on class "
+ clazz.getCanonicalName(),
e);
}
}
@SuppressWarnings("unchecked")
static <T extends Annotation> T createJsonQualifierImplementation(final Class<T> annotationType) {
if (!annotationType.isAnnotation()) {
@ -309,21 +200,103 @@ public final class Types {
});
}
/**
* 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 };
static boolean equal(Object a, Object b) {
return a == b || (a != null && a.equals(b));
}
Type mapType = getSupertype(context, contextRawType, Map.class);
if (mapType instanceof ParameterizedType) {
ParameterizedType mapParameterizedType = (ParameterizedType) mapType;
return mapParameterizedType.getActualTypeArguments();
/** Returns true if {@code a} and {@code b} are equal. */
static boolean equals(Type a, Type b) {
if (a == b) {
return true; // Also handles (a == null && b == null).
} else if (a instanceof Class) {
return a.equals(b); // Class already specifies equals().
} else if (a instanceof ParameterizedType) {
if (!(b instanceof ParameterizedType)) return false;
ParameterizedType pa = (ParameterizedType) a;
ParameterizedType pb = (ParameterizedType) b;
Type[] aTypeArguments = pa instanceof ParameterizedTypeImpl
? ((ParameterizedTypeImpl) pa).typeArguments
: pa.getActualTypeArguments();
Type[] bTypeArguments = pb instanceof ParameterizedTypeImpl
? ((ParameterizedTypeImpl) pb).typeArguments
: pb.getActualTypeArguments();
return equal(pa.getOwnerType(), pb.getOwnerType())
&& pa.getRawType().equals(pb.getRawType())
&& Arrays.equals(aTypeArguments, bTypeArguments);
} else if (a instanceof GenericArrayType) {
if (!(b instanceof GenericArrayType)) return false;
GenericArrayType ga = (GenericArrayType) a;
GenericArrayType gb = (GenericArrayType) b;
return equals(ga.getGenericComponentType(), gb.getGenericComponentType());
} else if (a instanceof WildcardType) {
if (!(b instanceof WildcardType)) return false;
WildcardType wa = (WildcardType) a;
WildcardType wb = (WildcardType) b;
return Arrays.equals(wa.getUpperBounds(), wb.getUpperBounds())
&& Arrays.equals(wa.getLowerBounds(), wb.getLowerBounds());
} else if (a instanceof TypeVariable) {
if (!(b instanceof TypeVariable)) return false;
TypeVariable<?> va = (TypeVariable<?>) a;
TypeVariable<?> vb = (TypeVariable<?>) b;
return va.getGenericDeclaration() == vb.getGenericDeclaration()
&& va.getName().equals(vb.getName());
} else {
// This isn't a supported type.
return false;
}
return new Type[] { Object.class, Object.class };
}
static int hashCodeOrZero(Object o) {
return o != null ? o.hashCode() : 0;
}
static String typeToString(Type type) {
return type instanceof Class ? ((Class<?>) type).getName() : type.toString();
}
/**
* Returns the generic supertype for {@code supertype}. For example, given a class {@code
* IntegerSet}, the result for when supertype is {@code Set.class} is {@code Set<Integer>} and the
* result when the supertype is {@code Collection.class} is {@code Collection<Integer>}.
*/
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;
}
/**
@ -357,4 +330,305 @@ public final class Types {
return null;
}
}
/**
* Returns the element type of this collection type.
* @throws IllegalArgumentException if this type is not a collection.
*/
public static Type collectionElementType(Type context, Class<?> contextRawType) {
Type collectionType = getSupertype(context, contextRawType, Collection.class);
if (collectionType instanceof WildcardType) {
collectionType = ((WildcardType) collectionType).getUpperBounds()[0];
}
if (collectionType instanceof ParameterizedType) {
return ((ParameterizedType) collectionType).getActualTypeArguments()[0];
}
return Object.class;
}
/**
* Returns a two element array containing this map's key and value types in positions 0 and 1
* respectively.
*/
static Type[] mapKeyAndValueTypes(Type context, Class<?> contextRawType) {
// Work around a problem with the declaration of java.util.Properties. That class should extend
// Hashtable<String, String>, but it's declared to extend Hashtable<Object, Object>.
if (context == Properties.class) return new Type[] { String.class, String.class };
Type mapType = getSupertype(context, contextRawType, Map.class);
if (mapType instanceof ParameterizedType) {
ParameterizedType mapParameterizedType = (ParameterizedType) mapType;
return mapParameterizedType.getActualTypeArguments();
}
return new Type[] { Object.class, Object.class };
}
static Type resolve(Type context, Class<?> contextRawType, Type toResolve) {
// This implementation is made a little more complicated in an attempt to avoid object-creation.
while (true) {
if (toResolve instanceof TypeVariable) {
TypeVariable<?> typeVariable = (TypeVariable<?>) toResolve;
toResolve = resolveTypeVariable(context, contextRawType, typeVariable);
if (toResolve == typeVariable) return toResolve;
} else if (toResolve instanceof Class && ((Class<?>) toResolve).isArray()) {
Class<?> original = (Class<?>) toResolve;
Type componentType = original.getComponentType();
Type newComponentType = resolve(context, contextRawType, componentType);
return componentType == newComponentType
? original
: arrayOf(newComponentType);
} else if (toResolve instanceof GenericArrayType) {
GenericArrayType original = (GenericArrayType) toResolve;
Type componentType = original.getGenericComponentType();
Type newComponentType = resolve(context, contextRawType, componentType);
return componentType == newComponentType
? original
: arrayOf(newComponentType);
} else if (toResolve instanceof ParameterizedType) {
ParameterizedType original = (ParameterizedType) toResolve;
Type ownerType = original.getOwnerType();
Type newOwnerType = resolve(context, contextRawType, ownerType);
boolean changed = newOwnerType != ownerType;
Type[] args = original.getActualTypeArguments();
for (int t = 0, length = args.length; t < length; t++) {
Type resolvedTypeArgument = resolve(context, contextRawType, args[t]);
if (resolvedTypeArgument != args[t]) {
if (!changed) {
args = args.clone();
changed = true;
}
args[t] = resolvedTypeArgument;
}
}
return changed
? new ParameterizedTypeImpl(newOwnerType, original.getRawType(), args)
: original;
} else if (toResolve instanceof WildcardType) {
WildcardType original = (WildcardType) toResolve;
Type[] originalLowerBound = original.getLowerBounds();
Type[] originalUpperBound = original.getUpperBounds();
if (originalLowerBound.length == 1) {
Type lowerBound = resolve(context, contextRawType, originalLowerBound[0]);
if (lowerBound != originalLowerBound[0]) {
return supertypeOf(lowerBound);
}
} else if (originalUpperBound.length == 1) {
Type upperBound = resolve(context, contextRawType, originalUpperBound[0]);
if (upperBound != originalUpperBound[0]) {
return subtypeOf(upperBound);
}
}
return original;
} else {
return toResolve;
}
}
}
static Type resolveTypeVariable(Type context, Class<?> contextRawType, TypeVariable<?> unknown) {
Class<?> declaredByRaw = declaringClassOf(unknown);
// We can't reduce this further.
if (declaredByRaw == null) return unknown;
Type declaredBy = getGenericSupertype(context, contextRawType, declaredByRaw);
if (declaredBy instanceof ParameterizedType) {
int index = indexOf(declaredByRaw.getTypeParameters(), unknown);
return ((ParameterizedType) declaredBy).getActualTypeArguments()[index];
}
return unknown;
}
/**
* Returns true if this is a Type supported by {@link StandardJsonAdapters#FACTORY}.
*/
static boolean isAllowedPlatformType(Type type) {
return type == Boolean.class
|| type == Byte.class
|| type == Character.class
|| type == Double.class
|| type == Float.class
|| type == Integer.class
|| type == Long.class
|| type == Short.class
|| type == String.class
|| type == Object.class;
}
private static int indexOf(Object[] array, Object toFind) {
for (int i = 0; i < array.length; i++) {
if (toFind.equals(array[i])) return i;
}
throw new NoSuchElementException();
}
/**
* Returns the declaring class of {@code typeVariable}, or {@code null} if it was not declared by
* a class.
*/
private static Class<?> declaringClassOf(TypeVariable<?> typeVariable) {
GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration();
return genericDeclaration instanceof Class ? (Class<?>) genericDeclaration : null;
}
static void checkNotPrimitive(Type type) {
if ((type instanceof Class<?>) && ((Class<?>) type).isPrimitive()) {
throw new IllegalArgumentException();
}
}
private static final class ParameterizedTypeImpl implements ParameterizedType {
private final Type ownerType;
private final Type rawType;
final Type[] typeArguments;
ParameterizedTypeImpl(Type ownerType, Type rawType, Type... typeArguments) {
// Require an owner type if the raw type needs it.
if (rawType instanceof Class<?>
&& (ownerType == null) != (((Class<?>) rawType).getEnclosingClass() == null)) {
throw new IllegalArgumentException(
"unexpected owner type for " + rawType + ": " + ownerType);
}
this.ownerType = ownerType == null ? null : canonicalize(ownerType);
this.rawType = canonicalize(rawType);
this.typeArguments = typeArguments.clone();
for (int t = 0; t < this.typeArguments.length; t++) {
if (this.typeArguments[t] == null) throw new NullPointerException();
checkNotPrimitive(this.typeArguments[t]);
this.typeArguments[t] = canonicalize(this.typeArguments[t]);
}
}
@Override public Type[] getActualTypeArguments() {
return typeArguments.clone();
}
@Override public Type getRawType() {
return rawType;
}
@Override public Type getOwnerType() {
return ownerType;
}
@Override public boolean equals(Object other) {
return other instanceof ParameterizedType
&& Types.equals(this, (ParameterizedType) other);
}
@Override public int hashCode() {
return Arrays.hashCode(typeArguments)
^ rawType.hashCode()
^ hashCodeOrZero(ownerType);
}
@Override public String toString() {
StringBuilder result = new StringBuilder(30 * (typeArguments.length + 1));
result.append(typeToString(rawType));
if (typeArguments.length == 0) {
return result.toString();
}
result.append("<").append(typeToString(typeArguments[0]));
for (int i = 1; i < typeArguments.length; i++) {
result.append(", ").append(typeToString(typeArguments[i]));
}
return result.append(">").toString();
}
}
private static final class GenericArrayTypeImpl implements GenericArrayType {
private final Type componentType;
public GenericArrayTypeImpl(Type componentType) {
this.componentType = canonicalize(componentType);
}
@Override public Type getGenericComponentType() {
return componentType;
}
@Override public boolean equals(Object o) {
return o instanceof GenericArrayType
&& Types.equals(this, (GenericArrayType) o);
}
@Override public int hashCode() {
return componentType.hashCode();
}
@Override public String toString() {
return typeToString(componentType) + "[]";
}
}
/**
* The WildcardType interface supports multiple upper bounds and multiple lower bounds. We only
* support what the Java 6 language needs - at most one bound. If a lower bound is set, the upper
* bound must be Object.class.
*/
private static final class WildcardTypeImpl implements WildcardType {
private final Type upperBound;
private final Type lowerBound;
public WildcardTypeImpl(Type[] upperBounds, Type[] lowerBounds) {
if (lowerBounds.length > 1) throw new IllegalArgumentException();
if (upperBounds.length != 1) throw new IllegalArgumentException();
if (lowerBounds.length == 1) {
if (lowerBounds[0] == null) throw new NullPointerException();
checkNotPrimitive(lowerBounds[0]);
if (upperBounds[0] != Object.class) throw new IllegalArgumentException();
this.lowerBound = canonicalize(lowerBounds[0]);
this.upperBound = Object.class;
} else {
if (upperBounds[0] == null) throw new NullPointerException();
checkNotPrimitive(upperBounds[0]);
this.lowerBound = null;
this.upperBound = canonicalize(upperBounds[0]);
}
}
@Override public Type[] getUpperBounds() {
return new Type[] { upperBound };
}
@Override public Type[] getLowerBounds() {
return lowerBound != null ? new Type[] { lowerBound } : EMPTY_TYPE_ARRAY;
}
@Override public boolean equals(Object other) {
return other instanceof WildcardType
&& Types.equals(this, (WildcardType) other);
}
@Override public int hashCode() {
// This equals Arrays.hashCode(getLowerBounds()) ^ Arrays.hashCode(getUpperBounds()).
return (lowerBound != null ? 31 + lowerBound.hashCode() : 1)
^ (31 + upperBound.hashCode());
}
@Override public String toString() {
if (lowerBound != null) {
return "? super " + typeToString(lowerBound);
} else if (upperBound == Object.class) {
return "?";
} else {
return "? extends " + typeToString(upperBound);
}
}
}
}

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,8 +15,6 @@
*/
package com.squareup.moshi;
import com.squareup.moshi.MoshiTest.Uppercase;
import com.squareup.moshi.MoshiTest.UppercaseAdapterFactory;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -26,10 +24,8 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import okio.ByteString;
import org.junit.Test;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@ -83,91 +79,6 @@ public final class AdapterMethodsTest {
}
}
private static final class PointJsonAdapterWithDelegate {
@FromJson Point fromJson(JsonReader reader, JsonAdapter<Point> delegate) throws IOException {
reader.beginArray();
Point value = delegate.fromJson(reader);
reader.endArray();
return value;
}
@ToJson void toJson(JsonWriter writer, Point value, JsonAdapter<Point> delegate)
throws IOException {
writer.beginArray();
delegate.toJson(writer, value);
writer.endArray();
}
}
private static final class PointJsonAdapterWithDelegateWithQualifier {
@FromJson @WithParens Point fromJson(JsonReader reader, @WithParens JsonAdapter<Point> delegate)
throws IOException {
reader.beginArray();
Point value = delegate.fromJson(reader);
reader.endArray();
return value;
}
@ToJson void toJson(JsonWriter writer, @WithParens Point value,
@WithParens JsonAdapter<Point> delegate)
throws IOException {
writer.beginArray();
delegate.toJson(writer, value);
writer.endArray();
}
}
@Test public void toAndFromWithDelegate() throws Exception {
Moshi moshi = new Moshi.Builder()
.add(new PointJsonAdapterWithDelegate())
.build();
JsonAdapter<Point> adapter = moshi.adapter(Point.class);
Point point = new Point(5, 8);
assertThat(adapter.toJson(point)).isEqualTo("[{\"x\":5,\"y\":8}]");
assertThat(adapter.fromJson("[{\"x\":5,\"y\":8}]")).isEqualTo(point);
}
@Test public void toAndFromWithDelegateWithQualifier() throws Exception {
Moshi moshi = new Moshi.Builder()
.add(new PointJsonAdapterWithDelegateWithQualifier())
.add(new PointWithParensJsonAdapter())
.build();
JsonAdapter<Point> adapter = moshi.adapter(Point.class, WithParens.class);
Point point = new Point(5, 8);
assertThat(adapter.toJson(point)).isEqualTo("[\"(5 8)\"]");
assertThat(adapter.fromJson("[\"(5 8)\"]")).isEqualTo(point);
}
@Test public void toAndFromWithIntermediate() throws Exception {
Moshi moshi = new Moshi.Builder().add(new Object() {
@FromJson String fromJson(String string) {
return string.substring(1, string.length() - 1);
}
@ToJson String toJson(String value) {
return "|" + value + "|";
}
}).build();
JsonAdapter<String> adapter = moshi.adapter(String.class);
assertThat(adapter.toJson("pizza")).isEqualTo("\"|pizza|\"");
assertThat(adapter.fromJson("\"|pizza|\"")).isEqualTo("pizza");
}
@Test public void toAndFromWithIntermediateWithQualifier() throws Exception {
Moshi moshi = new Moshi.Builder().add(new Object() {
@FromJson @Uppercase String fromJson(@Uppercase String string) {
return string.substring(1, string.length() - 1);
}
@ToJson @Uppercase String toJson(@Uppercase String value) {
return "|" + value + "|";
}
}).add(new UppercaseAdapterFactory()).build();
JsonAdapter<String> adapter = moshi.adapter(String.class, Uppercase.class);
assertThat(adapter.toJson("pizza")).isEqualTo("\"|PIZZA|\"");
assertThat(adapter.fromJson("\"|pizza|\"")).isEqualTo("PIZZA");
}
@Test public void toJsonOnly() throws Exception {
Moshi moshi = new Moshi.Builder()
.add(new PointAsListOfIntegersToAdapter())
@ -304,8 +215,6 @@ public final class AdapterMethodsTest {
+ "(com.squareup.moshi.AdapterMethodsTest$Point).\n"
+ "@ToJson method signatures may have one of the following structures:\n"
+ " <any access modifier> void toJson(JsonWriter writer, T value) throws <any>;\n"
+ " <any access modifier> void toJson(JsonWriter writer, T value,"
+ " JsonAdapter<any> delegate, <any more delegates>) throws <any>;\n"
+ " <any access modifier> R toJson(T value) throws <any>;\n");
}
}
@ -325,9 +234,7 @@ public final class AdapterMethodsTest {
+ "com.squareup.moshi.AdapterMethodsTest$UnexpectedSignatureFromJsonAdapter.pointFromJson"
+ "(java.lang.String).\n"
+ "@FromJson method signatures may have one of the following structures:\n"
+ " <any access modifier> 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> void fromJson(JsonReader jsonReader) throws <any>;\n"
+ " <any access modifier> R fromJson(T value) throws <any>;\n");
}
}
@ -397,7 +304,7 @@ public final class AdapterMethodsTest {
}
static class NullableIntToJsonAdapter {
@FromJson int jsonToInt(JsonReader reader) throws IOException {
@FromJson int intToJson(JsonReader reader) throws IOException {
if (reader.peek() == JsonReader.Token.NULL) {
reader.nextNull();
return -1;
@ -405,7 +312,7 @@ public final class AdapterMethodsTest {
return reader.nextInt();
}
@ToJson void intToJson(JsonWriter writer, int value) throws IOException {
@ToJson void jsonToInt(JsonWriter writer, int value) throws IOException {
if (value == -1) {
writer.nullValue();
} else {
@ -462,10 +369,7 @@ public final class AdapterMethodsTest {
fail();
} catch (IllegalArgumentException e) {
assertThat(e).hasMessage("No @FromJson adapter for interface "
+ "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)");
assertThat(e).hasCauseExactlyInstanceOf(IllegalArgumentException.class);
assertThat(e.getCause()).hasMessage("No next JsonAdapter for interface "
+ "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)");
+ "com.squareup.moshi.AdapterMethodsTest$Shape annotated []");
}
}
@ -484,10 +388,7 @@ public final class AdapterMethodsTest {
fail();
} catch (IllegalArgumentException e) {
assertThat(e).hasMessage("No @ToJson adapter for interface "
+ "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)");
assertThat(e).hasCauseExactlyInstanceOf(IllegalArgumentException.class);
assertThat(e.getCause()).hasMessage("No next JsonAdapter for interface "
+ "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)");
+ "com.squareup.moshi.AdapterMethodsTest$Shape annotated []");
}
}
@ -714,57 +615,6 @@ public final class AdapterMethodsTest {
}
}
@Test public void genericArrayTypes() throws Exception {
Moshi moshi = new Moshi.Builder()
.add(new ByteArrayJsonAdapter())
.build();
JsonAdapter<MapOfByteArrays> jsonAdapter = moshi.adapter(MapOfByteArrays.class);
MapOfByteArrays mapOfByteArrays = new MapOfByteArrays(
Collections.singletonMap("a", new byte[] { 0, -1}));
String json = "{\"map\":{\"a\":\"00ff\"}}";
assertThat(jsonAdapter.toJson(mapOfByteArrays)).isEqualTo(json);
assertThat(jsonAdapter.fromJson(json)).isEqualTo(mapOfByteArrays);
}
static class ByteArrayJsonAdapter {
@ToJson String byteArrayToJson(byte[] b) {
return ByteString.of(b).hex();
}
@FromJson byte[] byteArrayFromJson(String s) throws Exception {
return ByteString.decodeHex(s).toByteArray();
}
}
static class MapOfByteArrays {
final Map<String, byte[]> map;
public MapOfByteArrays(Map<String, byte[]> map) {
this.map = map;
}
@Override public boolean equals(Object o) {
return o instanceof MapOfByteArrays && o.toString().equals(toString());
}
@Override public int hashCode() {
return toString().hashCode();
}
@Override public String toString() {
StringBuilder result = new StringBuilder();
for (Map.Entry<String, byte[]> entry : map.entrySet()) {
if (result.length() > 0) result.append(", ");
result.append(entry.getKey())
.append(":")
.append(Arrays.toString(entry.getValue()));
}
return result.toString();
}
}
static class Point {
final int x;
final int y;

View file

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

View file

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

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

@ -21,7 +21,6 @@ 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;
@ -95,45 +94,6 @@ public final class JsonAdapterTest {
assertThat(factory.json()).isEqualTo("[\"A\",null,\"C\"]");
}
@Test public void nonNull() throws Exception {
JsonAdapter<String> toUpperCase = new JsonAdapter<String>() {
@Override public String fromJson(JsonReader reader) throws IOException {
return reader.nextString().toUpperCase(Locale.US);
}
@Override public void toJson(JsonWriter writer, String value) throws IOException {
writer.value(value.toUpperCase(Locale.US));
}
}.nonNull();
JsonReader reader = factory.newReader("[\"a\", null, \"c\"]");
reader.beginArray();
assertThat(toUpperCase.fromJson(reader)).isEqualTo("A");
try {
toUpperCase.fromJson(reader);
fail();
} catch (JsonDataException expected) {
assertThat(expected).hasMessage("Unexpected null at $[1]");
assertThat(reader.nextNull()).isNull();
}
assertThat(toUpperCase.fromJson(reader)).isEqualTo("C");
reader.endArray();
JsonWriter writer = factory.newWriter();
writer.beginArray();
toUpperCase.toJson(writer, "a");
try {
toUpperCase.toJson(writer, null);
fail();
} catch (JsonDataException expected) {
assertThat(expected).hasMessage("Unexpected null at $[1]");
writer.nullValue();
}
toUpperCase.toJson(writer, "c");
writer.endArray();
assertThat(factory.json()).isEqualTo("[\"A\",null,\"C\"]");
}
@Test public void failOnUnknown() throws Exception {
JsonAdapter<String> alwaysSkip = new JsonAdapter<String>() {
@Override public String fromJson(JsonReader reader) throws IOException {
@ -185,24 +145,6 @@ public final class JsonAdapterTest {
+ "]");
}
@Test public void indentDisallowsNull() throws Exception {
JsonAdapter<Object> adapter = new JsonAdapter<Object>() {
@Override public Object fromJson(JsonReader reader) {
throw new AssertionError();
}
@Override public void toJson(JsonWriter writer, Object value) {
throw new AssertionError();
}
};
try {
adapter.indent(null);
fail();
} catch (NullPointerException expected) {
assertThat(expected).hasMessage("indent == null");
}
}
@Test public void serializeNulls() throws Exception {
JsonAdapter<Map<String, String>> serializeNulls = new JsonAdapter<Map<String, String>>() {
@Override public Map<String, String> fromJson(JsonReader reader) throws IOException {
@ -222,67 +164,4 @@ public final class JsonAdapterTest {
serializeNulls.toJson(writer, Collections.<String, String>singletonMap("a", null));
assertThat(factory.json()).isEqualTo("{\"a\":null}");
}
@Test public void stringDocumentMustBeFullyConsumed() throws IOException {
JsonAdapter<String> brokenAdapter = new JsonAdapter<String>() {
@Override public String fromJson(JsonReader reader) throws IOException {
return "Forgot to call reader.nextString().";
}
@Override public void toJson(JsonWriter writer, @Nullable String value) throws IOException {
throw new AssertionError();
}
};
try {
brokenAdapter.fromJson("\"value\"");
fail();
} catch (JsonDataException e) {
assertThat(e).hasMessage("JSON document was not fully consumed.");
}
}
@Test public void adapterFromJsonStringPeeksAtEnd() throws IOException {
JsonAdapter<Boolean> adapter = new JsonAdapter<Boolean>() {
@Override public Boolean fromJson(JsonReader reader) throws IOException {
return reader.nextBoolean();
}
@Override public void toJson(JsonWriter writer, @Nullable Boolean value) throws IOException {
throw new AssertionError();
}
};
try {
adapter.fromJson("true true");
fail();
} catch (JsonEncodingException e) {
assertThat(e).hasMessage(
"Use JsonReader.setLenient(true) to accept malformed JSON at path $");
}
}
@Test public void lenientAdapterFromJsonStringDoesNotPeekAtEnd() throws IOException {
JsonAdapter<Boolean> adapter = new JsonAdapter<Boolean>() {
@Override public Boolean fromJson(JsonReader reader) throws IOException {
return reader.nextBoolean();
}
@Override public void toJson(JsonWriter writer, @Nullable Boolean value) throws IOException {
throw new AssertionError();
}
}.lenient();
assertThat(adapter.fromJson("true true")).isEqualTo(true);
}
@Test public void adaptersDelegateLeniency() throws IOException {
JsonAdapter<Boolean> adapter = new JsonAdapter<Boolean>() {
@Override public Boolean fromJson(JsonReader reader) throws IOException {
return reader.nextBoolean();
}
@Override public void toJson(JsonWriter writer, @Nullable Boolean value) throws IOException {
throw new AssertionError();
}
}.lenient().nonNull();
assertThat(adapter.fromJson("true true")).isEqualTo(true);
}
}

View file

@ -96,38 +96,9 @@ abstract class JsonCodecFactory {
}
};
final JsonCodecFactory valuePeek = new JsonCodecFactory() {
@Override public JsonReader newReader(String json) throws IOException {
return value.newReader(json).peekJson();
}
// TODO(jwilson): fix precision checks and delete his method.
@Override boolean implementsStrictPrecision() {
return false;
}
@Override JsonWriter newWriter() {
return value.newWriter();
}
@Override String json() {
return value.json();
}
// TODO(jwilson): support BigDecimal and BigInteger and delete his method.
@Override boolean supportsBigNumbers() {
return false;
}
@Override public String toString() {
return "ValuePeek";
}
};
return Arrays.asList(
new Object[] { utf8 },
new Object[] { value },
new Object[] { valuePeek });
new Object[] { value });
}
abstract JsonReader newReader(String json) throws IOException;

View file

@ -332,16 +332,8 @@ public final class JsonQualifiersTest {
moshi.adapter(StringAndFooString.class);
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("No @FromJson adapter for class java.lang.String annotated "
+ "[@com.squareup.moshi.JsonQualifiersTest$FooPrefix()]"
+ "\nfor class java.lang.String b"
+ "\nfor class com.squareup.moshi.JsonQualifiersTest$StringAndFooString");
assertThat(expected).hasCauseExactlyInstanceOf(IllegalArgumentException.class);
assertThat(expected.getCause()).hasMessage("No @FromJson adapter for class java.lang.String "
assertThat(expected).hasMessage("No @FromJson adapter for class java.lang.String "
+ "annotated [@com.squareup.moshi.JsonQualifiersTest$FooPrefix()]");
assertThat(expected.getCause()).hasCauseExactlyInstanceOf(IllegalArgumentException.class);
assertThat(expected.getCause().getCause()).hasMessage("No next JsonAdapter for class "
+ "java.lang.String annotated [@com.squareup.moshi.JsonQualifiersTest$FooPrefix()]");
}
}
@ -361,16 +353,8 @@ public final class JsonQualifiersTest {
moshi.adapter(StringAndFooString.class);
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("No @ToJson adapter for class java.lang.String annotated "
+ "[@com.squareup.moshi.JsonQualifiersTest$FooPrefix()]"
+ "\nfor class java.lang.String b"
+ "\nfor class com.squareup.moshi.JsonQualifiersTest$StringAndFooString");
assertThat(expected).hasCauseExactlyInstanceOf(IllegalArgumentException.class);
assertThat(expected.getCause()).hasMessage("No @ToJson adapter for class java.lang.String "
assertThat(expected).hasMessage("No @ToJson adapter for class java.lang.String "
+ "annotated [@com.squareup.moshi.JsonQualifiersTest$FooPrefix()]");
assertThat(expected.getCause()).hasCauseExactlyInstanceOf(IllegalArgumentException.class);
assertThat(expected.getCause().getCause()).hasMessage("No next JsonAdapter for class "
+ "java.lang.String annotated [@com.squareup.moshi.JsonQualifiersTest$FooPrefix()]");
}
}

View file

@ -35,7 +35,6 @@ public final class JsonReaderPathTest {
return JsonCodecFactory.factories();
}
@SuppressWarnings("CheckReturnValue")
@Test public void path() throws IOException {
JsonReader reader = factory.newReader("{\"a\":[2,true,false,null,\"b\",{\"c\":\"d\"},[3]]}");
assertThat(reader.getPath()).isEqualTo("$");
@ -115,7 +114,6 @@ public final class JsonReaderPathTest {
assertThat(reader.getPath()).isEqualTo("$");
}
@SuppressWarnings("CheckReturnValue")
@Test public void objectPath() throws IOException {
JsonReader reader = factory.newReader("{\"a\":1,\"b\":2}");
assertThat(reader.getPath()).isEqualTo("$");
@ -156,7 +154,6 @@ public final class JsonReaderPathTest {
assertThat(reader.getPath()).isEqualTo("$");
}
@SuppressWarnings("CheckReturnValue")
@Test public void arrayPath() throws IOException {
JsonReader reader = factory.newReader("[1,2]");
assertThat(reader.getPath()).isEqualTo("$");
@ -215,7 +212,6 @@ public final class JsonReaderPathTest {
assertThat(reader.getPath()).isEqualTo("$.null");
}
@SuppressWarnings("CheckReturnValue")
@Test public void skipObjectValues() throws IOException {
JsonReader reader = factory.newReader("{\"a\":1,\"b\":2}");
reader.beginObject();

View file

@ -33,12 +33,10 @@ import static com.squareup.moshi.JsonReader.Token.STRING;
import static com.squareup.moshi.TestUtil.repeat;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
@RunWith(Parameterized.class)
@SuppressWarnings("CheckReturnValue")
public final class JsonReaderTest {
@Parameter public JsonCodecFactory factory;
@ -854,21 +852,6 @@ public final class JsonReaderTest {
reader.endArray();
}
@Test public void selectStringWithoutString() throws IOException {
JsonReader.Options numbers = JsonReader.Options.of("1", "2.0", "true", "4");
JsonReader reader = newReader("[0, 2.0, true, \"4\"]");
reader.beginArray();
assertThat(reader.selectString(numbers)).isEqualTo(-1);
reader.skipValue();
assertThat(reader.selectString(numbers)).isEqualTo(-1);
reader.skipValue();
assertThat(reader.selectString(numbers)).isEqualTo(-1);
reader.skipValue();
assertThat(reader.selectString(numbers)).isEqualTo(3);
reader.endArray();
}
@Test public void stringToNumberCoersion() throws Exception {
JsonReader reader = newReader("[\"0\", \"9223372036854775807\", \"1.5\"]");
reader.beginArray();
@ -962,227 +945,4 @@ public final class JsonReaderTest {
assertThat(value).isEqualTo(
Collections.singletonMap("pizzas", Arrays.asList("cheese", "pepperoni")));
}
@Test public void skipName() throws IOException {
JsonReader reader = newReader("{\"a\":1}");
reader.beginObject();
reader.skipName();
assertThat(reader.peek()).isEqualTo(JsonReader.Token.NUMBER);
reader.skipValue();
reader.endObject();
}
@Test public void skipNameOnValueFails() throws IOException {
JsonReader reader = newReader("1");
try {
reader.skipName();
fail();
} catch (JsonDataException expected) {
}
assertThat(reader.nextInt()).isEqualTo(1);
}
@Test public void emptyDocumentHasNextReturnsFalse() throws IOException {
JsonReader reader = newReader("1");
reader.readJsonValue();
assertThat(reader.hasNext()).isFalse();
}
@Test public void skipValueAtEndOfObjectFails() throws IOException {
JsonReader reader = newReader("{}");
reader.beginObject();
try {
reader.skipValue();
fail();
} catch (JsonDataException expected) {
assertThat(expected).hasMessage("Expected a value but was END_OBJECT at path $.");
}
reader.endObject();
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
}
@Test public void skipValueAtEndOfArrayFails() throws IOException {
JsonReader reader = newReader("[]");
reader.beginArray();
try {
reader.skipValue();
fail();
} catch (JsonDataException expected) {
assertThat(expected).hasMessage("Expected a value but was END_ARRAY at path $[0]");
}
reader.endArray();
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
}
@Test public void skipValueAtEndOfDocumentFails() throws IOException {
JsonReader reader = newReader("1");
reader.nextInt();
try {
reader.skipValue();
fail();
} catch (JsonDataException expected) {
assertThat(expected).hasMessage("Expected a value but was END_DOCUMENT at path $");
}
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
}
@Test public void basicPeekJson() throws IOException {
JsonReader reader = newReader("{\"a\":12,\"b\":[34,56],\"c\":78}");
reader.beginObject();
assertThat(reader.nextName()).isEqualTo("a");
assertThat(reader.nextInt()).isEqualTo(12);
assertThat(reader.nextName()).isEqualTo("b");
reader.beginArray();
assertThat(reader.nextInt()).isEqualTo(34);
// Peek.
JsonReader peekReader = reader.peekJson();
assertThat(peekReader.nextInt()).isEqualTo(56);
peekReader.endArray();
assertThat(peekReader.nextName()).isEqualTo("c");
assertThat(peekReader.nextInt()).isEqualTo(78);
peekReader.endObject();
assertThat(peekReader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
// Read again.
assertThat(reader.nextInt()).isEqualTo(56);
reader.endArray();
assertThat(reader.nextName()).isEqualTo("c");
assertThat(reader.nextInt()).isEqualTo(78);
reader.endObject();
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
}
/**
* We have a document that requires 12 operations to read. We read it step-by-step with one real
* reader. Before each of the real readers operations we create a peeking reader and let it read
* the rest of the document.
*/
@Test public void peekJsonReader() throws IOException {
JsonReader reader = newReader("[12,34,{\"a\":56,\"b\":78},90]");
for (int i = 0; i < 12; i++) {
readPeek12Steps(reader.peekJson(), i, 12);
readPeek12Steps(reader, i, i + 1);
}
}
/**
* Read a fragment of {@code reader}. This assumes the fixed document defined in {@link
* #peekJsonReader} and reads a range of it on each call.
*/
private void readPeek12Steps(JsonReader reader, int from, int until) throws IOException {
switch (from) {
case 0:
if (until == 0) break;
reader.beginArray();
assertThat(reader.getPath()).isEqualTo("$[0]");
case 1:
if (until == 1) break;
assertThat(reader.nextInt()).isEqualTo(12);
assertThat(reader.getPath()).isEqualTo("$[1]");
case 2:
if (until == 2) break;
assertThat(reader.nextInt()).isEqualTo(34);
assertThat(reader.getPath()).isEqualTo("$[2]");
case 3:
if (until == 3) break;
reader.beginObject();
assertThat(reader.getPath()).isEqualTo("$[2].");
case 4:
if (until == 4) break;
assertThat(reader.nextName()).isEqualTo("a");
assertThat(reader.getPath()).isEqualTo("$[2].a");
case 5:
if (until == 5) break;
assertThat(reader.nextInt()).isEqualTo(56);
assertThat(reader.getPath()).isEqualTo("$[2].a");
case 6:
if (until == 6) break;
assertThat(reader.nextName()).isEqualTo("b");
assertThat(reader.getPath()).isEqualTo("$[2].b");
case 7:
if (until == 7) break;
assertThat(reader.nextInt()).isEqualTo(78);
assertThat(reader.getPath()).isEqualTo("$[2].b");
case 8:
if (until == 8) break;
reader.endObject();
assertThat(reader.getPath()).isEqualTo("$[3]");
case 9:
if (until == 9) break;
assertThat(reader.nextInt()).isEqualTo(90);
assertThat(reader.getPath()).isEqualTo("$[4]");
case 10:
if (until == 10) break;
reader.endArray();
assertThat(reader.getPath()).isEqualTo("$");
case 11:
if (until == 11) break;
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
assertThat(reader.getPath()).isEqualTo("$");
}
}
/** Confirm that we can peek in every state of the UTF-8 reader. */
@Test public void peekAfterPeek() throws IOException {
JsonReader reader = newReader(
"[{\"a\":\"aaa\",'b':'bbb',c:c,\"d\":\"d\"},true,false,null,1,2.0]");
reader.setLenient(true);
readValue(reader, true);
reader.peekJson();
}
@Test public void peekAfterPromoteNameToValue() throws IOException {
JsonReader reader = newReader("{\"a\":\"b\"}");
reader.beginObject();
reader.promoteNameToValue();
assertEquals("a", reader.peekJson().nextString());
assertEquals("a", reader.nextString());
assertEquals("b", reader.peekJson().nextString());
assertEquals("b", reader.nextString());
reader.endObject();
}
/** Peek a value, then read it, recursively. */
private void readValue(JsonReader reader, boolean peekJsonFirst) throws IOException {
JsonReader.Token token = reader.peek();
if (peekJsonFirst) {
readValue(reader.peekJson(), false);
}
switch (token) {
case BEGIN_ARRAY:
reader.beginArray();
while (reader.hasNext()) {
readValue(reader, peekJsonFirst);
}
reader.peekJson().endArray();
reader.endArray();
break;
case BEGIN_OBJECT:
reader.beginObject();
while (reader.hasNext()) {
assertNotNull(reader.peekJson().nextName());
assertNotNull(reader.nextName());
readValue(reader, peekJsonFirst);
}
reader.peekJson().endObject();
reader.endObject();
break;
case STRING:
reader.nextString();
break;
case NUMBER:
reader.nextDouble();
break;
case BOOLEAN:
reader.nextBoolean();
break;
case NULL:
reader.nextNull();
break;
default:
throw new AssertionError();
}
}
}

Some files were not shown because too many files have changed in this diff Show more