mirror of
https://github.com/fankes/moshi.git
synced 2025-10-18 23:49:21 +08:00
Convert adapters to Kotlin (#1460)
* Convert Rfc3339DateJsonAdapter * Convert EnumJsonAdapter * Convert PolymorphicJsonAdapterFactory * Convert Rfc3339DateJsonAdapter and Iso8601Utils * Doc indent fix * Use template * Address CR comments * Spotless and jsr cleanup * Couple small tweaks * Remove toList() * Use simpler map * Inline GregorianCalendar * Interp * Fix copyright * interp * Fix another copyright * Restore toList()
This commit is contained in:
@@ -13,7 +13,6 @@ dependencies {
|
||||
compileOnly(libs.jsr305)
|
||||
api(project(":moshi"))
|
||||
|
||||
testCompileOnly(libs.jsr305)
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.truth)
|
||||
}
|
||||
|
@@ -1,38 +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
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.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}.
|
||||
*/
|
||||
public final class Rfc3339DateJsonAdapter extends JsonAdapter<Date> {
|
||||
private final 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 void toJson(JsonWriter writer, Date value) throws IOException {
|
||||
delegate.toJson(writer, value);
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* https://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.adapters.Rfc3339DateJsonAdapter
|
||||
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 com.squareup.moshi.adapters.Rfc3339DateJsonAdapter.""",
|
||||
replaceWith = ReplaceWith("com.squareup.moshi.adapters.Rfc3339DateJsonAdapter"),
|
||||
level = DeprecationLevel.ERROR
|
||||
)
|
||||
public class Rfc3339DateJsonAdapter : JsonAdapter<Date>() {
|
||||
|
||||
private val delegate = Rfc3339DateJsonAdapter()
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun fromJson(reader: JsonReader): Date? {
|
||||
return delegate.fromJson(reader)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun toJson(writer: JsonWriter, value: Date?) {
|
||||
delegate.toJson(writer, value)
|
||||
}
|
||||
}
|
@@ -1,116 +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
|
||||
*
|
||||
* https://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 static com.squareup.moshi.internal.Util.jsonName;
|
||||
|
||||
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();
|
||||
nameStrings[i] = jsonName(enumType.getField(constantName), constantName);
|
||||
}
|
||||
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() + ")";
|
||||
}
|
||||
}
|
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* https://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.JsonReader.Options
|
||||
import com.squareup.moshi.JsonReader.Token.STRING
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import com.squareup.moshi.internal.jsonName
|
||||
import java.io.IOException
|
||||
import java.lang.NoSuchFieldException
|
||||
|
||||
/**
|
||||
* 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
|
||||
* [Moshi.Builder][com.squareup.moshi.Moshi.Builder]:
|
||||
*
|
||||
* ```
|
||||
* Moshi moshi = new Moshi.Builder()
|
||||
* .add(CurrencyCode.class, EnumJsonAdapter.create(CurrencyCode.class)
|
||||
* .withUnknownFallback(CurrencyCode.USD))
|
||||
* .build();
|
||||
* ```
|
||||
*/
|
||||
public class EnumJsonAdapter<T : Enum<T>> internal constructor(
|
||||
private val enumType: Class<T>,
|
||||
private val fallbackValue: T?,
|
||||
private val useFallbackValue: Boolean,
|
||||
) : JsonAdapter<T>() {
|
||||
|
||||
private val constants: Array<T>
|
||||
private val options: Options
|
||||
private val nameStrings: Array<String>
|
||||
|
||||
init {
|
||||
try {
|
||||
constants = enumType.enumConstants
|
||||
nameStrings = Array(constants.size) { i ->
|
||||
val constantName = constants[i].name
|
||||
enumType.getField(constantName).jsonName(constantName)
|
||||
}
|
||||
options = Options.of(*nameStrings)
|
||||
} catch (e: NoSuchFieldException) {
|
||||
throw AssertionError("Missing field in ${enumType.name}", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 fun withUnknownFallback(fallbackValue: T?): EnumJsonAdapter<T> {
|
||||
return EnumJsonAdapter(enumType, fallbackValue, useFallbackValue = true)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun fromJson(reader: JsonReader): T? {
|
||||
val index = reader.selectString(options)
|
||||
if (index != -1) return constants[index]
|
||||
val path = reader.path
|
||||
if (!useFallbackValue) {
|
||||
val name = reader.nextString()
|
||||
throw JsonDataException(
|
||||
"Expected one of ${nameStrings.toList()} but was $name at path $path"
|
||||
)
|
||||
}
|
||||
if (reader.peek() != STRING) {
|
||||
throw JsonDataException(
|
||||
"Expected a string but was ${reader.peek()} at path $path"
|
||||
)
|
||||
}
|
||||
reader.skipValue()
|
||||
return fallbackValue
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun toJson(writer: JsonWriter, value: T?) {
|
||||
if (value == null) {
|
||||
throw NullPointerException(
|
||||
"value was null! Wrap in .nullSafe() to write nullable values."
|
||||
)
|
||||
}
|
||||
writer.value(nameStrings[value.ordinal])
|
||||
}
|
||||
|
||||
override fun toString(): String = "EnumJsonAdapter(${enumType.name})"
|
||||
|
||||
public companion object {
|
||||
@JvmStatic
|
||||
public fun <T : Enum<T>> create(enumType: Class<T>): EnumJsonAdapter<T> {
|
||||
return EnumJsonAdapter(enumType, fallbackValue = null, useFallbackValue = false)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,275 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2011 FasterXML, LLC
|
||||
*
|
||||
* 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.JsonDataException;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* Jackson’s date formatter, pruned to Moshi's needs. Forked from this file:
|
||||
* https://github.com/FasterXML/jackson-databind/blob/67ebf7305f492285a8f9f4de31545f5f16fc7c3a/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java
|
||||
*
|
||||
* <p>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
|
||||
* objects.
|
||||
*
|
||||
* <p>Supported parse format:
|
||||
* [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh[:]mm]]
|
||||
*
|
||||
* @see <a href="http://www.w3.org/TR/NOTE-datetime">this specification</a>
|
||||
*/
|
||||
final class Iso8601Utils {
|
||||
/** ID to represent the 'GMT' string */
|
||||
static final String GMT_ID = "GMT";
|
||||
|
||||
/** The GMT timezone, prefetched to avoid more lookups. */
|
||||
static final TimeZone TIMEZONE_Z = TimeZone.getTimeZone(GMT_ID);
|
||||
|
||||
/** Returns {@code date} formatted as yyyy-MM-ddThh:mm:ss.sssZ */
|
||||
public static String format(Date date) {
|
||||
Calendar calendar = new GregorianCalendar(TIMEZONE_Z, Locale.US);
|
||||
calendar.setTime(date);
|
||||
|
||||
// estimate capacity of buffer as close as we can (yeah, that's pedantic ;)
|
||||
int capacity = "yyyy-MM-ddThh:mm:ss.sssZ".length();
|
||||
StringBuilder formatted = new StringBuilder(capacity);
|
||||
padInt(formatted, calendar.get(Calendar.YEAR), "yyyy".length());
|
||||
formatted.append('-');
|
||||
padInt(formatted, calendar.get(Calendar.MONTH) + 1, "MM".length());
|
||||
formatted.append('-');
|
||||
padInt(formatted, calendar.get(Calendar.DAY_OF_MONTH), "dd".length());
|
||||
formatted.append('T');
|
||||
padInt(formatted, calendar.get(Calendar.HOUR_OF_DAY), "hh".length());
|
||||
formatted.append(':');
|
||||
padInt(formatted, calendar.get(Calendar.MINUTE), "mm".length());
|
||||
formatted.append(':');
|
||||
padInt(formatted, calendar.get(Calendar.SECOND), "ss".length());
|
||||
formatted.append('.');
|
||||
padInt(formatted, calendar.get(Calendar.MILLISECOND), "sss".length());
|
||||
formatted.append('Z');
|
||||
return formatted.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a date from ISO-8601 formatted string. It expects a format
|
||||
* [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh:mm]]
|
||||
*
|
||||
* @param date ISO string to parse in the appropriate format.
|
||||
* @return the parsed date
|
||||
*/
|
||||
public static Date parse(String date) {
|
||||
try {
|
||||
int offset = 0;
|
||||
|
||||
// extract year
|
||||
int year = parseInt(date, offset, offset += 4);
|
||||
if (checkOffset(date, offset, '-')) {
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
// extract month
|
||||
int month = parseInt(date, offset, offset += 2);
|
||||
if (checkOffset(date, offset, '-')) {
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
// extract day
|
||||
int day = parseInt(date, offset, offset += 2);
|
||||
// default time value
|
||||
int hour = 0;
|
||||
int minutes = 0;
|
||||
int seconds = 0;
|
||||
int milliseconds =
|
||||
0; // always use 0 otherwise returned date will include millis of current time
|
||||
|
||||
// if the value has no time component (and no time zone), we are done
|
||||
boolean hasT = checkOffset(date, offset, 'T');
|
||||
|
||||
if (!hasT && (date.length() <= offset)) {
|
||||
Calendar calendar = new GregorianCalendar(year, month - 1, day);
|
||||
|
||||
return calendar.getTime();
|
||||
}
|
||||
|
||||
if (hasT) {
|
||||
|
||||
// extract hours, minutes, seconds and milliseconds
|
||||
hour = parseInt(date, offset += 1, offset += 2);
|
||||
if (checkOffset(date, offset, ':')) {
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
minutes = parseInt(date, offset, offset += 2);
|
||||
if (checkOffset(date, offset, ':')) {
|
||||
offset += 1;
|
||||
}
|
||||
// second and milliseconds can be optional
|
||||
if (date.length() > offset) {
|
||||
char c = date.charAt(offset);
|
||||
if (c != 'Z' && c != '+' && c != '-') {
|
||||
seconds = parseInt(date, offset, offset += 2);
|
||||
if (seconds > 59 && seconds < 63) seconds = 59; // truncate up to 3 leap seconds
|
||||
// milliseconds can be optional in the format
|
||||
if (checkOffset(date, offset, '.')) {
|
||||
offset += 1;
|
||||
int endOffset = indexOfNonDigit(date, offset + 1); // assume at least one digit
|
||||
int parseEndOffset = Math.min(endOffset, offset + 3); // parse up to 3 digits
|
||||
int fraction = parseInt(date, offset, parseEndOffset);
|
||||
milliseconds = (int) (Math.pow(10, 3 - (parseEndOffset - offset)) * fraction);
|
||||
offset = endOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extract timezone
|
||||
if (date.length() <= offset) {
|
||||
throw new IllegalArgumentException("No time zone indicator");
|
||||
}
|
||||
|
||||
TimeZone timezone;
|
||||
char timezoneIndicator = date.charAt(offset);
|
||||
|
||||
if (timezoneIndicator == 'Z') {
|
||||
timezone = TIMEZONE_Z;
|
||||
} else if (timezoneIndicator == '+' || timezoneIndicator == '-') {
|
||||
String timezoneOffset = date.substring(offset);
|
||||
// 18-Jun-2015, tatu: Minor simplification, skip offset of "+0000"/"+00:00"
|
||||
if ("+0000".equals(timezoneOffset) || "+00:00".equals(timezoneOffset)) {
|
||||
timezone = TIMEZONE_Z;
|
||||
} else {
|
||||
// 18-Jun-2015, tatu: Looks like offsets only work from GMT, not UTC...
|
||||
// not sure why, but it is what it is.
|
||||
String timezoneId = GMT_ID + timezoneOffset;
|
||||
timezone = TimeZone.getTimeZone(timezoneId);
|
||||
String act = timezone.getID();
|
||||
if (!act.equals(timezoneId)) {
|
||||
/* 22-Jan-2015, tatu: Looks like canonical version has colons, but we may be given
|
||||
* one without. If so, don't sweat.
|
||||
* Yes, very inefficient. Hopefully not hit often.
|
||||
* If it becomes a perf problem, add 'loose' comparison instead.
|
||||
*/
|
||||
String cleaned = act.replace(":", "");
|
||||
if (!cleaned.equals(timezoneId)) {
|
||||
throw new IndexOutOfBoundsException(
|
||||
"Mismatching time zone indicator: "
|
||||
+ timezoneId
|
||||
+ " given, resolves to "
|
||||
+ timezone.getID());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new IndexOutOfBoundsException(
|
||||
"Invalid time zone indicator '" + timezoneIndicator + "'");
|
||||
}
|
||||
|
||||
Calendar calendar = new GregorianCalendar(timezone);
|
||||
calendar.setLenient(false);
|
||||
calendar.set(Calendar.YEAR, year);
|
||||
calendar.set(Calendar.MONTH, month - 1);
|
||||
calendar.set(Calendar.DAY_OF_MONTH, day);
|
||||
calendar.set(Calendar.HOUR_OF_DAY, hour);
|
||||
calendar.set(Calendar.MINUTE, minutes);
|
||||
calendar.set(Calendar.SECOND, seconds);
|
||||
calendar.set(Calendar.MILLISECOND, milliseconds);
|
||||
|
||||
return calendar.getTime();
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the expected character exist at the given offset in the value.
|
||||
*
|
||||
* @param value the string to check at the specified offset
|
||||
* @param offset the offset to look for the expected character
|
||||
* @param expected the expected character
|
||||
* @return true if the expected character exist at the given offset
|
||||
*/
|
||||
private static boolean checkOffset(String value, int offset, char expected) {
|
||||
return (offset < value.length()) && (value.charAt(offset) == expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an integer located between 2 given offsets in a string
|
||||
*
|
||||
* @param value the string to parse
|
||||
* @param beginIndex the start index for the integer in the string
|
||||
* @param endIndex the end index for the integer in the string
|
||||
* @return the int
|
||||
* @throws NumberFormatException if the value is not a number
|
||||
*/
|
||||
private static int parseInt(String value, int beginIndex, int endIndex)
|
||||
throws NumberFormatException {
|
||||
if (beginIndex < 0 || endIndex > value.length() || beginIndex > endIndex) {
|
||||
throw new NumberFormatException(value);
|
||||
}
|
||||
// use same logic as in Integer.parseInt() but less generic we're not supporting negative values
|
||||
int i = beginIndex;
|
||||
int result = 0;
|
||||
int digit;
|
||||
if (i < endIndex) {
|
||||
digit = Character.digit(value.charAt(i++), 10);
|
||||
if (digit < 0) {
|
||||
throw new NumberFormatException("Invalid number: " + value.substring(beginIndex, endIndex));
|
||||
}
|
||||
result = -digit;
|
||||
}
|
||||
while (i < endIndex) {
|
||||
digit = Character.digit(value.charAt(i++), 10);
|
||||
if (digit < 0) {
|
||||
throw new NumberFormatException("Invalid number: " + value.substring(beginIndex, endIndex));
|
||||
}
|
||||
result *= 10;
|
||||
result -= digit;
|
||||
}
|
||||
return -result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zero pad a number to a specified length
|
||||
*
|
||||
* @param buffer buffer to use for padding
|
||||
* @param value the integer value to pad if necessary.
|
||||
* @param length the length of the string we should zero pad
|
||||
*/
|
||||
private static void padInt(StringBuilder buffer, int value, int length) {
|
||||
String strValue = Integer.toString(value);
|
||||
for (int i = length - strValue.length(); i > 0; i--) {
|
||||
buffer.append('0');
|
||||
}
|
||||
buffer.append(strValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the first character in the string that is not a digit, starting at offset.
|
||||
*/
|
||||
private static int indexOfNonDigit(String string, int offset) {
|
||||
for (int i = offset; i < string.length(); i++) {
|
||||
char c = string.charAt(i);
|
||||
if (c < '0' || c > '9') return i;
|
||||
}
|
||||
return string.length();
|
||||
}
|
||||
}
|
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
* Copyright (C) 2011 FasterXML, LLC
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.JsonDataException
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.GregorianCalendar
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import kotlin.math.min
|
||||
import kotlin.math.pow
|
||||
|
||||
/*
|
||||
* Jackson’s date formatter, pruned to Moshi's needs. Forked from this file:
|
||||
* https://github.com/FasterXML/jackson-databind/blob/67ebf7305f492285a8f9f4de31545f5f16fc7c3a/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java
|
||||
*
|
||||
* 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
|
||||
* objects.
|
||||
*
|
||||
* Supported parse format:
|
||||
* `[yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh[:]mm]]`
|
||||
*
|
||||
* @see [this specification](http://www.w3.org/TR/NOTE-datetime)
|
||||
*/
|
||||
|
||||
/** ID to represent the 'GMT' string */
|
||||
private const val GMT_ID = "GMT"
|
||||
|
||||
/** The GMT timezone, prefetched to avoid more lookups. */
|
||||
private val TIMEZONE_Z: TimeZone = TimeZone.getTimeZone(GMT_ID)
|
||||
|
||||
/** Returns `date` formatted as yyyy-MM-ddThh:mm:ss.sssZ */
|
||||
internal fun Date.formatIsoDate(): String {
|
||||
val calendar: Calendar = GregorianCalendar(TIMEZONE_Z, Locale.US)
|
||||
calendar.time = this
|
||||
|
||||
// estimate capacity of buffer as close as we can (yeah, that's pedantic ;)
|
||||
val capacity = "yyyy-MM-ddThh:mm:ss.sssZ".length
|
||||
val formatted = StringBuilder(capacity)
|
||||
padInt(formatted, calendar[Calendar.YEAR], "yyyy".length)
|
||||
formatted.append('-')
|
||||
padInt(formatted, calendar[Calendar.MONTH] + 1, "MM".length)
|
||||
formatted.append('-')
|
||||
padInt(formatted, calendar[Calendar.DAY_OF_MONTH], "dd".length)
|
||||
formatted.append('T')
|
||||
padInt(formatted, calendar[Calendar.HOUR_OF_DAY], "hh".length)
|
||||
formatted.append(':')
|
||||
padInt(formatted, calendar[Calendar.MINUTE], "mm".length)
|
||||
formatted.append(':')
|
||||
padInt(formatted, calendar[Calendar.SECOND], "ss".length)
|
||||
formatted.append('.')
|
||||
padInt(formatted, calendar[Calendar.MILLISECOND], "sss".length)
|
||||
formatted.append('Z')
|
||||
return formatted.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a date from ISO-8601 formatted string. It expects a format
|
||||
* `[yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh:mm]]`
|
||||
*
|
||||
* @receiver ISO string to parse in the appropriate format.
|
||||
* @return the parsed date
|
||||
*/
|
||||
internal fun String.parseIsoDate(): Date {
|
||||
return try {
|
||||
var offset = 0
|
||||
|
||||
// extract year
|
||||
val year = parseInt(this, offset, 4.let { offset += it; offset })
|
||||
if (checkOffset(this, offset, '-')) {
|
||||
offset += 1
|
||||
}
|
||||
|
||||
// extract month
|
||||
val month = parseInt(this, offset, 2.let { offset += it; offset })
|
||||
if (checkOffset(this, offset, '-')) {
|
||||
offset += 1
|
||||
}
|
||||
|
||||
// extract day
|
||||
val day = parseInt(this, offset, 2.let { offset += it; offset })
|
||||
// default time value
|
||||
var hour = 0
|
||||
var minutes = 0
|
||||
var seconds = 0
|
||||
// always use 0 otherwise returned date will include millis of current time
|
||||
var milliseconds = 0
|
||||
|
||||
// if the value has no time component (and no time zone), we are done
|
||||
val hasT = checkOffset(this, offset, 'T')
|
||||
if (!hasT && this.length <= offset) {
|
||||
return GregorianCalendar(year, month - 1, day).time
|
||||
}
|
||||
if (hasT) {
|
||||
|
||||
// extract hours, minutes, seconds and milliseconds
|
||||
hour = parseInt(this, 1.let { offset += it; offset }, 2.let { offset += it; offset })
|
||||
if (checkOffset(this, offset, ':')) {
|
||||
offset += 1
|
||||
}
|
||||
minutes = parseInt(this, offset, 2.let { offset += it; offset })
|
||||
if (checkOffset(this, offset, ':')) {
|
||||
offset += 1
|
||||
}
|
||||
// second and milliseconds can be optional
|
||||
if (this.length > offset) {
|
||||
val c = this[offset]
|
||||
if (c != 'Z' && c != '+' && c != '-') {
|
||||
seconds = parseInt(this, offset, 2.let { offset += it; offset })
|
||||
if (seconds in 60..62) seconds = 59 // truncate up to 3 leap seconds
|
||||
// milliseconds can be optional in the format
|
||||
if (checkOffset(this, offset, '.')) {
|
||||
offset += 1
|
||||
val endOffset = indexOfNonDigit(this, offset + 1) // assume at least one digit
|
||||
val parseEndOffset = min(endOffset, offset + 3) // parse up to 3 digits
|
||||
val fraction = parseInt(this, offset, parseEndOffset)
|
||||
milliseconds =
|
||||
(10.0.pow((3 - (parseEndOffset - offset)).toDouble()) * fraction).toInt()
|
||||
offset = endOffset
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extract timezone
|
||||
require(this.length > offset) { "No time zone indicator" }
|
||||
val timezone: TimeZone
|
||||
val timezoneIndicator = this[offset]
|
||||
if (timezoneIndicator == 'Z') {
|
||||
timezone = TIMEZONE_Z
|
||||
} else if (timezoneIndicator == '+' || timezoneIndicator == '-') {
|
||||
val timezoneOffset = this.substring(offset)
|
||||
// 18-Jun-2015, tatu: Minor simplification, skip offset of "+0000"/"+00:00"
|
||||
if ("+0000" == timezoneOffset || "+00:00" == timezoneOffset) {
|
||||
timezone = TIMEZONE_Z
|
||||
} else {
|
||||
// 18-Jun-2015, tatu: Looks like offsets only work from GMT, not UTC...
|
||||
// not sure why, but it is what it is.
|
||||
val timezoneId = GMT_ID + timezoneOffset
|
||||
timezone = TimeZone.getTimeZone(timezoneId)
|
||||
val act = timezone.id
|
||||
if (act != timezoneId) {
|
||||
/*
|
||||
* 22-Jan-2015, tatu: Looks like canonical version has colons, but we may be given
|
||||
* one without. If so, don't sweat.
|
||||
* Yes, very inefficient. Hopefully not hit often.
|
||||
* If it becomes a perf problem, add 'loose' comparison instead.
|
||||
*/
|
||||
val cleaned = act.replace(":", "")
|
||||
if (cleaned != timezoneId) {
|
||||
throw IndexOutOfBoundsException(
|
||||
"Mismatching time zone indicator: $timezoneId given, resolves to ${timezone.id}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw IndexOutOfBoundsException(
|
||||
"Invalid time zone indicator '$timezoneIndicator'"
|
||||
)
|
||||
}
|
||||
val calendar: Calendar = GregorianCalendar(timezone)
|
||||
calendar.isLenient = false
|
||||
calendar[Calendar.YEAR] = year
|
||||
calendar[Calendar.MONTH] = month - 1
|
||||
calendar[Calendar.DAY_OF_MONTH] = day
|
||||
calendar[Calendar.HOUR_OF_DAY] = hour
|
||||
calendar[Calendar.MINUTE] = minutes
|
||||
calendar[Calendar.SECOND] = seconds
|
||||
calendar[Calendar.MILLISECOND] = milliseconds
|
||||
calendar.time
|
||||
// If we get a ParseException it'll already have the right message/offset.
|
||||
// Other exception types can convert here.
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
throw JsonDataException("Not an RFC 3339 date: $this", e)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw JsonDataException("Not an RFC 3339 date: $this", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the expected character exist at the given offset in the value.
|
||||
*
|
||||
* @param value the string to check at the specified offset
|
||||
* @param offset the offset to look for the expected character
|
||||
* @param expected the expected character
|
||||
* @return true if the expected character exist at the given offset
|
||||
*/
|
||||
private fun checkOffset(value: String, offset: Int, expected: Char): Boolean {
|
||||
return offset < value.length && value[offset] == expected
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an integer located between 2 given offsets in a string
|
||||
*
|
||||
* @param value the string to parse
|
||||
* @param beginIndex the start index for the integer in the string
|
||||
* @param endIndex the end index for the integer in the string
|
||||
* @return the int
|
||||
* @throws NumberFormatException if the value is not a number
|
||||
*/
|
||||
private fun parseInt(value: String, beginIndex: Int, endIndex: Int): Int {
|
||||
if (beginIndex < 0 || endIndex > value.length || beginIndex > endIndex) {
|
||||
throw NumberFormatException(value)
|
||||
}
|
||||
// use same logic as in Integer.parseInt() but less generic we're not supporting negative values
|
||||
var i = beginIndex
|
||||
var result = 0
|
||||
var digit: Int
|
||||
if (i < endIndex) {
|
||||
digit = Character.digit(value[i++], 10)
|
||||
if (digit < 0) {
|
||||
throw NumberFormatException("Invalid number: " + value.substring(beginIndex, endIndex))
|
||||
}
|
||||
result = -digit
|
||||
}
|
||||
while (i < endIndex) {
|
||||
digit = Character.digit(value[i++], 10)
|
||||
if (digit < 0) {
|
||||
throw NumberFormatException("Invalid number: " + value.substring(beginIndex, endIndex))
|
||||
}
|
||||
result *= 10
|
||||
result -= digit
|
||||
}
|
||||
return -result
|
||||
}
|
||||
|
||||
/**
|
||||
* Zero pad a number to a specified length
|
||||
*
|
||||
* @param buffer buffer to use for padding
|
||||
* @param value the integer value to pad if necessary.
|
||||
* @param length the length of the string we should zero pad
|
||||
*/
|
||||
private fun padInt(buffer: StringBuilder, value: Int, length: Int) {
|
||||
val strValue = value.toString()
|
||||
for (i in length - strValue.length downTo 1) {
|
||||
buffer.append('0')
|
||||
}
|
||||
buffer.append(strValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the first character in the string that is not a digit, starting at offset.
|
||||
*/
|
||||
private fun indexOfNonDigit(string: String, offset: Int): Int {
|
||||
for (i in offset until string.length) {
|
||||
val c = string[i]
|
||||
if (c < '0' || c > '9') return i
|
||||
}
|
||||
return string.length
|
||||
}
|
@@ -1,330 +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
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.squareup.moshi.adapters;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.JsonDataException;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.JsonWriter;
|
||||
import com.squareup.moshi.Moshi;
|
||||
import com.squareup.moshi.Types;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import javax.annotation.CheckReturnValue;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* A JsonAdapter factory for objects that include type information in the JSON. When decoding JSON
|
||||
* Moshi uses this type information to determine which class to decode to. When encoding Moshi uses
|
||||
* the object’s class to determine what type information to include.
|
||||
*
|
||||
* <p>Suppose we have an interface, its implementations, and a class that uses them:
|
||||
*
|
||||
* <pre>{@code
|
||||
* interface HandOfCards {
|
||||
* }
|
||||
*
|
||||
* class BlackjackHand implements HandOfCards {
|
||||
* Card hidden_card;
|
||||
* List<Card> visible_cards;
|
||||
* }
|
||||
*
|
||||
* class HoldemHand implements 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:
|
||||
*
|
||||
* <ul>
|
||||
* <li>If {@link #withDefaultValue(Object)} is used, then {@code defaultValue} will be returned.
|
||||
* <li>If {@link #withFallbackJsonAdapter(JsonAdapter)} is used, then the {@code
|
||||
* fallbackJsonAdapter.fromJson(reader)} result will be returned.
|
||||
* <li>Otherwise a {@link JsonDataException} will be thrown.
|
||||
* </ul>
|
||||
*
|
||||
* <p>If an unknown type is encountered when encoding:
|
||||
*
|
||||
* <ul>
|
||||
* <li>If {@link #withFallbackJsonAdapter(JsonAdapter)} is used, then the {@code
|
||||
* fallbackJsonAdapter.toJson(writer, value)} result will be returned.
|
||||
* <li>Otherwise a {@link IllegalArgumentException} will be thrown.
|
||||
* </ul>
|
||||
*
|
||||
* <p>If the same subtype has multiple labels the first one is used when encoding.
|
||||
*/
|
||||
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 JsonAdapter<Object> fallbackJsonAdapter;
|
||||
|
||||
PolymorphicJsonAdapterFactory(
|
||||
Class<T> baseType,
|
||||
String labelKey,
|
||||
List<String> labels,
|
||||
List<Type> subtypes,
|
||||
@Nullable JsonAdapter<Object> fallbackJsonAdapter) {
|
||||
this.baseType = baseType;
|
||||
this.labelKey = labelKey;
|
||||
this.labels = labels;
|
||||
this.subtypes = subtypes;
|
||||
this.fallbackJsonAdapter = fallbackJsonAdapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
|
||||
/** Returns a new factory that decodes instances of {@code subtype}. */
|
||||
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, fallbackJsonAdapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new factory that with default to {@code fallbackJsonAdapter.fromJson(reader)} upon
|
||||
* decoding of unrecognized labels.
|
||||
*
|
||||
* <p>The {@link JsonReader} instance will not be automatically consumed, so make sure to consume
|
||||
* it within your implementation of {@link JsonAdapter#fromJson(JsonReader)}
|
||||
*/
|
||||
public PolymorphicJsonAdapterFactory<T> withFallbackJsonAdapter(
|
||||
@Nullable JsonAdapter<Object> fallbackJsonAdapter) {
|
||||
return new PolymorphicJsonAdapterFactory<>(
|
||||
baseType, labelKey, labels, subtypes, fallbackJsonAdapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new factory that will default to {@code defaultValue} upon decoding of unrecognized
|
||||
* labels. The default value should be immutable.
|
||||
*/
|
||||
public PolymorphicJsonAdapterFactory<T> withDefaultValue(@Nullable T defaultValue) {
|
||||
return withFallbackJsonAdapter(buildFallbackJsonAdapter(defaultValue));
|
||||
}
|
||||
|
||||
private JsonAdapter<Object> buildFallbackJsonAdapter(final T defaultValue) {
|
||||
return new JsonAdapter<Object>() {
|
||||
@Override
|
||||
public @Nullable Object fromJson(JsonReader reader) throws IOException {
|
||||
reader.skipValue();
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toJson(JsonWriter writer, Object value) throws IOException {
|
||||
throw new IllegalArgumentException(
|
||||
"Expected one of "
|
||||
+ subtypes
|
||||
+ " but found "
|
||||
+ value
|
||||
+ ", a "
|
||||
+ value.getClass()
|
||||
+ ". Register this subtype.");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@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, fallbackJsonAdapter)
|
||||
.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 JsonAdapter<Object> fallbackJsonAdapter;
|
||||
|
||||
/** 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 JsonAdapter<Object> fallbackJsonAdapter) {
|
||||
this.labelKey = labelKey;
|
||||
this.labels = labels;
|
||||
this.subtypes = subtypes;
|
||||
this.jsonAdapters = jsonAdapters;
|
||||
this.fallbackJsonAdapter = fallbackJsonAdapter;
|
||||
|
||||
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) {
|
||||
return this.fallbackJsonAdapter.fromJson(reader);
|
||||
} else {
|
||||
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 && this.fallbackJsonAdapter == null) {
|
||||
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);
|
||||
final JsonAdapter<Object> adapter;
|
||||
if (labelIndex == -1) {
|
||||
if (fallbackJsonAdapter == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Expected one of "
|
||||
+ subtypes
|
||||
+ " but found "
|
||||
+ value
|
||||
+ ", a "
|
||||
+ value.getClass()
|
||||
+ ". Register this subtype.");
|
||||
}
|
||||
adapter = fallbackJsonAdapter;
|
||||
} else {
|
||||
adapter = jsonAdapters.get(labelIndex);
|
||||
}
|
||||
|
||||
writer.beginObject();
|
||||
if (adapter != fallbackJsonAdapter) {
|
||||
writer.name(labelKey).value(labels.get(labelIndex));
|
||||
}
|
||||
int flattenToken = writer.beginFlatten();
|
||||
adapter.toJson(writer, value);
|
||||
writer.endFlatten(flattenToken);
|
||||
writer.endObject();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PolymorphicJsonAdapter(" + labelKey + ")";
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* https://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.JsonAdapter.Factory
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonReader.Options
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.rawType
|
||||
import java.io.IOException
|
||||
import java.lang.reflect.Type
|
||||
import javax.annotation.CheckReturnValue
|
||||
|
||||
/**
|
||||
* A JsonAdapter factory for objects that include type information in the JSON. When decoding JSON
|
||||
* Moshi uses this type information to determine which class to decode to. When encoding Moshi uses
|
||||
* the object’s class to determine what type information to include.
|
||||
*
|
||||
* Suppose we have an interface, its implementations, and a class that uses them:
|
||||
*
|
||||
* ```
|
||||
* interface HandOfCards { }
|
||||
*
|
||||
* class BlackjackHand implements HandOfCards {
|
||||
* Card hidden_card;
|
||||
* List<Card> visible_cards;
|
||||
* }
|
||||
*
|
||||
* class HoldemHand implements HandOfCards {
|
||||
* Set<Card> hidden_cards;
|
||||
* }
|
||||
*
|
||||
* class Player {
|
||||
* String name;
|
||||
* HandOfCards hand;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* We want to decode the following JSON into the player model above:
|
||||
*
|
||||
* ```
|
||||
* {
|
||||
* "name": "Jesse",
|
||||
* "hand": {
|
||||
* "hand_type": "blackjack",
|
||||
* "hidden_card": "9D",
|
||||
* "visible_cards": ["8H", "4C"]
|
||||
* }
|
||||
* }
|
||||
*```
|
||||
*
|
||||
* Left unconfigured, Moshi would incorrectly attempt to decode the hand object to the abstract
|
||||
* `HandOfCards` interface. We configure it to use the appropriate subtype instead:
|
||||
*
|
||||
* ```
|
||||
* Moshi moshi = new Moshi.Builder()
|
||||
* .add(PolymorphicJsonAdapterFactory.of(HandOfCards.class, "hand_type")
|
||||
* .withSubtype(BlackjackHand.class, "blackjack")
|
||||
* .withSubtype(HoldemHand.class, "holdem"))
|
||||
* .build();
|
||||
* ```
|
||||
*
|
||||
* This class imposes strict requirements on its use:
|
||||
* * Base types may be classes or interfaces.
|
||||
* * Subtypes must encode as JSON objects.
|
||||
* * Type information must be in the encoded object. Each message must have a type label like
|
||||
* `hand_type` whose value is a string like `blackjack` that identifies which type
|
||||
* to use.
|
||||
* * Each type identifier must be unique.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* If an unknown subtype is encountered when decoding:
|
||||
* * If [withDefaultValue] is used, then `defaultValue` will be returned.
|
||||
* * If [withFallbackJsonAdapter] is used, then the `fallbackJsonAdapter.fromJson(reader)` result will be returned.
|
||||
* * Otherwise a [JsonDataException] will be thrown.
|
||||
*
|
||||
* If an unknown type is encountered when encoding:
|
||||
* * If [withFallbackJsonAdapter] is used, then the `fallbackJsonAdapter.toJson(writer, value)` result will be returned.
|
||||
* * Otherwise a [IllegalArgumentException] will be thrown.
|
||||
*
|
||||
* If the same subtype has multiple labels the first one is used when encoding.
|
||||
*/
|
||||
public class PolymorphicJsonAdapterFactory<T> internal constructor(
|
||||
private val baseType: Class<T>,
|
||||
private val labelKey: String,
|
||||
private val labels: List<String>,
|
||||
private val subtypes: List<Type>,
|
||||
private val fallbackJsonAdapter: JsonAdapter<Any>?
|
||||
) : Factory {
|
||||
/** Returns a new factory that decodes instances of `subtype`. */
|
||||
public fun withSubtype(subtype: Class<out T>, label: String): PolymorphicJsonAdapterFactory<T> {
|
||||
require(!labels.contains(label)) { "Labels must be unique." }
|
||||
val newLabels = buildList {
|
||||
addAll(labels)
|
||||
add(label)
|
||||
}
|
||||
val newSubtypes = buildList {
|
||||
addAll(subtypes)
|
||||
add(subtype)
|
||||
}
|
||||
return PolymorphicJsonAdapterFactory(
|
||||
baseType = baseType,
|
||||
labelKey = labelKey,
|
||||
labels = newLabels,
|
||||
subtypes = newSubtypes,
|
||||
fallbackJsonAdapter = fallbackJsonAdapter
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new factory that with default to `fallbackJsonAdapter.fromJson(reader)` upon
|
||||
* decoding of unrecognized labels.
|
||||
*
|
||||
* The [JsonReader] instance will not be automatically consumed, so make sure to consume
|
||||
* it within your implementation of [JsonAdapter.fromJson]
|
||||
*/
|
||||
public fun withFallbackJsonAdapter(
|
||||
fallbackJsonAdapter: JsonAdapter<Any>?
|
||||
): PolymorphicJsonAdapterFactory<T> {
|
||||
return PolymorphicJsonAdapterFactory(
|
||||
baseType = baseType,
|
||||
labelKey = labelKey,
|
||||
labels = labels,
|
||||
subtypes = subtypes,
|
||||
fallbackJsonAdapter = fallbackJsonAdapter
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new factory that will default to `defaultValue` upon decoding of unrecognized
|
||||
* labels. The default value should be immutable.
|
||||
*/
|
||||
public fun withDefaultValue(defaultValue: T?): PolymorphicJsonAdapterFactory<T> {
|
||||
return withFallbackJsonAdapter(buildFallbackJsonAdapter(defaultValue))
|
||||
}
|
||||
|
||||
private fun buildFallbackJsonAdapter(defaultValue: T?): JsonAdapter<Any> {
|
||||
return object : JsonAdapter<Any>() {
|
||||
override fun fromJson(reader: JsonReader): Any? {
|
||||
reader.skipValue()
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
override fun toJson(writer: JsonWriter, value: Any?) {
|
||||
throw IllegalArgumentException(
|
||||
"Expected one of $subtypes but found $value, a ${value?.javaClass}. Register this subtype."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun create(type: Type, annotations: Set<Annotation?>, moshi: Moshi): JsonAdapter<*>? {
|
||||
if (type.rawType != baseType || annotations.isNotEmpty()) {
|
||||
return null
|
||||
}
|
||||
val jsonAdapters: List<JsonAdapter<Any>> = subtypes.map(moshi::adapter)
|
||||
return PolymorphicJsonAdapter(labelKey, labels, subtypes, jsonAdapters, fallbackJsonAdapter)
|
||||
.nullSafe()
|
||||
}
|
||||
|
||||
internal class PolymorphicJsonAdapter(
|
||||
private val labelKey: String,
|
||||
private val labels: List<String>,
|
||||
private val subtypes: List<Type>,
|
||||
private val jsonAdapters: List<JsonAdapter<Any>>,
|
||||
private val fallbackJsonAdapter: JsonAdapter<Any>?
|
||||
) : JsonAdapter<Any>() {
|
||||
/** Single-element options containing the label's key only. */
|
||||
private val labelKeyOptions: Options = Options.of(labelKey)
|
||||
|
||||
/** Corresponds to subtypes. */
|
||||
private val labelOptions: Options = Options.of(*labels.toTypedArray())
|
||||
|
||||
override fun fromJson(reader: JsonReader): Any? {
|
||||
val peeked = reader.peekJson()
|
||||
peeked.setFailOnUnknown(false)
|
||||
val labelIndex = peeked.use(::labelIndex)
|
||||
return if (labelIndex == -1) {
|
||||
fallbackJsonAdapter?.fromJson(reader)
|
||||
} else {
|
||||
jsonAdapters[labelIndex].fromJson(reader)
|
||||
}
|
||||
}
|
||||
|
||||
private fun labelIndex(reader: JsonReader): Int {
|
||||
reader.beginObject()
|
||||
while (reader.hasNext()) {
|
||||
if (reader.selectName(labelKeyOptions) == -1) {
|
||||
reader.skipName()
|
||||
reader.skipValue()
|
||||
continue
|
||||
}
|
||||
val labelIndex = reader.selectString(labelOptions)
|
||||
if (labelIndex == -1 && fallbackJsonAdapter == null) {
|
||||
throw JsonDataException(
|
||||
"Expected one of $labels for key '$labelKey' but found '${reader.nextString()}'. Register a subtype for this label."
|
||||
)
|
||||
}
|
||||
return labelIndex
|
||||
}
|
||||
throw JsonDataException("Missing label for $labelKey")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun toJson(writer: JsonWriter, value: Any?) {
|
||||
val type: Class<*> = value!!.javaClass
|
||||
val labelIndex = subtypes.indexOf(type)
|
||||
val adapter: JsonAdapter<Any> = if (labelIndex == -1) {
|
||||
requireNotNull(fallbackJsonAdapter) {
|
||||
"Expected one of $subtypes but found $value, a ${value.javaClass}. Register this subtype."
|
||||
}
|
||||
} else {
|
||||
jsonAdapters[labelIndex]
|
||||
}
|
||||
writer.beginObject()
|
||||
if (adapter !== fallbackJsonAdapter) {
|
||||
writer.name(labelKey).value(labels[labelIndex])
|
||||
}
|
||||
val flattenToken = writer.beginFlatten()
|
||||
adapter.toJson(writer, value)
|
||||
writer.endFlatten(flattenToken)
|
||||
writer.endObject()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "PolymorphicJsonAdapter($labelKey)"
|
||||
}
|
||||
}
|
||||
|
||||
public companion object {
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
@JvmStatic
|
||||
@CheckReturnValue
|
||||
public fun <T> of(baseType: Class<T>, labelKey: String): PolymorphicJsonAdapterFactory<T> {
|
||||
return PolymorphicJsonAdapterFactory(
|
||||
baseType = baseType,
|
||||
labelKey = labelKey,
|
||||
labels = emptyList(),
|
||||
subtypes = emptyList(),
|
||||
fallbackJsonAdapter = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,54 +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
|
||||
*
|
||||
* https://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}. This adapter is null-safe. 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 {
|
||||
if (reader.peek() == JsonReader.Token.NULL) {
|
||||
return reader.nextNull();
|
||||
}
|
||||
String string = reader.nextString();
|
||||
return Iso8601Utils.parse(string);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void toJson(JsonWriter writer, Date value) throws IOException {
|
||||
if (value == null) {
|
||||
writer.nullValue();
|
||||
} else {
|
||||
String string = Iso8601Utils.format(value);
|
||||
writer.value(string);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* https://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.JsonReader.Token.NULL
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import java.io.IOException
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Formats dates using [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt), which is
|
||||
* formatted like `2015-09-26T18:23:50.250Z`. This adapter is null-safe. To use, add this as
|
||||
* an adapter for `Date.class` on your [Moshi.Builder][com.squareup.moshi.Moshi.Builder]:
|
||||
*
|
||||
* ```
|
||||
* Moshi moshi = new Moshi.Builder()
|
||||
* .add(Date.class, new Rfc3339DateJsonAdapter())
|
||||
* .build();
|
||||
* ```
|
||||
*/
|
||||
public class Rfc3339DateJsonAdapter : JsonAdapter<Date>() {
|
||||
@Synchronized
|
||||
@Throws(IOException::class)
|
||||
override fun fromJson(reader: JsonReader): Date? {
|
||||
if (reader.peek() == NULL) {
|
||||
return reader.nextNull()
|
||||
}
|
||||
val string = reader.nextString()
|
||||
return string.parseIsoDate()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@Throws(IOException::class)
|
||||
override fun toJson(writer: JsonWriter, value: Date?) {
|
||||
if (value == null) {
|
||||
writer.nullValue()
|
||||
} else {
|
||||
val string = value.formatIsoDate()
|
||||
writer.value(string)
|
||||
}
|
||||
}
|
||||
}
|
@@ -26,8 +26,8 @@ import com.squareup.moshi.Moshi;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
import okio.Buffer;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.junit.Test;
|
||||
|
||||
@SuppressWarnings("CheckReturnValue")
|
||||
|
Reference in New Issue
Block a user