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:
Zac Sweers
2022-01-06 15:15:48 -05:00
committed by GitHub
parent e2c346e1de
commit 484d525db4
12 changed files with 743 additions and 815 deletions

View File

@@ -13,7 +13,6 @@ dependencies {
compileOnly(libs.jsr305)
api(project(":moshi"))
testCompileOnly(libs.jsr305)
testImplementation(libs.junit)
testImplementation(libs.truth)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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