use moshis Rfc3339DateJsonAdapter for json date parsing (#2584)
* Rename .java to .kt * use moshis Rfc3339DateJsonAdapter for json date parsing
This commit is contained in:
parent
fd568aedba
commit
3417a7272a
4 changed files with 319 additions and 286 deletions
|
@ -23,7 +23,7 @@ import com.google.gson.Gson
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.keylesspalace.tusky.BuildConfig
|
import com.keylesspalace.tusky.BuildConfig
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.json.UtcDateTypeAdapter
|
import com.keylesspalace.tusky.json.Rfc3339DateJsonAdapter
|
||||||
import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor
|
import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.network.MediaUploadApi
|
import com.keylesspalace.tusky.network.MediaUploadApi
|
||||||
|
@ -54,7 +54,7 @@ class NetworkModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesGson(): Gson = GsonBuilder()
|
fun providesGson(): Gson = GsonBuilder()
|
||||||
.registerTypeAdapter(Date::class.java, UtcDateTypeAdapter())
|
.registerTypeAdapter(Date::class.java, Rfc3339DateJsonAdapter())
|
||||||
.create()
|
.create()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
268
app/src/main/java/com/keylesspalace/tusky/json/Iso8601Utils.kt
Normal file
268
app/src/main/java/com/keylesspalace/tusky/json/Iso8601Utils.kt
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
package com.keylesspalace.tusky.json
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import com.google.gson.JsonParseException
|
||||||
|
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 JsonParseException("Not an RFC 3339 date: $this", e)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
throw JsonParseException("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
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
// https://github.com/google/gson/blob/master/extras/src/main/java/com/google/gson/typeadapters/UtcDateTypeAdapter.java
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2011 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.keylesspalace.tusky.json
|
||||||
|
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import com.google.gson.stream.JsonReader
|
||||||
|
import com.google.gson.stream.JsonToken
|
||||||
|
import com.google.gson.stream.JsonWriter
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
class Rfc3339DateJsonAdapter : TypeAdapter<Date?>() {
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun write(writer: JsonWriter, date: Date?) {
|
||||||
|
if (date == null) {
|
||||||
|
writer.nullValue()
|
||||||
|
} else {
|
||||||
|
writer.value(date.formatIsoDate())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun read(reader: JsonReader): Date? {
|
||||||
|
return when (reader.peek()) {
|
||||||
|
JsonToken.NULL -> {
|
||||||
|
reader.nextNull()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
reader.nextString().parseIsoDate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,284 +0,0 @@
|
||||||
// https://github.com/google/gson/blob/master/extras/src/main/java/com/google/gson/typeadapters/UtcDateTypeAdapter.java
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) 2011 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.keylesspalace.tusky.json;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.text.ParseException;
|
|
||||||
import java.text.ParsePosition;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.GregorianCalendar;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.TimeZone;
|
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
|
||||||
import com.google.gson.TypeAdapter;
|
|
||||||
import com.google.gson.stream.JsonReader;
|
|
||||||
import com.google.gson.stream.JsonWriter;
|
|
||||||
|
|
||||||
public final class UtcDateTypeAdapter extends TypeAdapter<Date> {
|
|
||||||
private final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone("UTC");
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void write(JsonWriter out, Date date) throws IOException {
|
|
||||||
if (date == null) {
|
|
||||||
out.nullValue();
|
|
||||||
} else {
|
|
||||||
String value = format(date, true, UTC_TIME_ZONE);
|
|
||||||
out.value(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Date read(JsonReader in) throws IOException {
|
|
||||||
try {
|
|
||||||
switch (in.peek()) {
|
|
||||||
case NULL:
|
|
||||||
in.nextNull();
|
|
||||||
return null;
|
|
||||||
default:
|
|
||||||
String date = in.nextString();
|
|
||||||
// Instead of using iso8601Format.parse(value), we use Jackson's date parsing
|
|
||||||
// This is because Android doesn't support XXX because it is JDK 1.6
|
|
||||||
return parse(date, new ParsePosition(0));
|
|
||||||
}
|
|
||||||
} catch (ParseException e) {
|
|
||||||
throw new JsonParseException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Date parsing code from Jackson databind ISO8601Utils.java
|
|
||||||
// https://github.com/FasterXML/jackson-databind/blob/master/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java
|
|
||||||
private static final String GMT_ID = "GMT";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format date into yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm]
|
|
||||||
*
|
|
||||||
* @param date the date to format
|
|
||||||
* @param millis true to include millis precision otherwise false
|
|
||||||
* @param tz timezone to use for the formatting (GMT will produce 'Z')
|
|
||||||
* @return the date formatted as yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm]
|
|
||||||
*/
|
|
||||||
private static String format(Date date, boolean millis, TimeZone tz) {
|
|
||||||
Calendar calendar = new GregorianCalendar(tz, 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".length();
|
|
||||||
capacity += millis ? ".sss".length() : 0;
|
|
||||||
capacity += tz.getRawOffset() == 0 ? "Z".length() : "+hh:mm".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());
|
|
||||||
if (millis) {
|
|
||||||
formatted.append('.');
|
|
||||||
padInt(formatted, calendar.get(Calendar.MILLISECOND), "sss".length());
|
|
||||||
}
|
|
||||||
|
|
||||||
int offset = tz.getOffset(calendar.getTimeInMillis());
|
|
||||||
if (offset != 0) {
|
|
||||||
int hours = Math.abs((offset / (60 * 1000)) / 60);
|
|
||||||
int minutes = Math.abs((offset / (60 * 1000)) % 60);
|
|
||||||
formatted.append(offset < 0 ? '-' : '+');
|
|
||||||
padInt(formatted, hours, "hh".length());
|
|
||||||
formatted.append(':');
|
|
||||||
padInt(formatted, minutes, "mm".length());
|
|
||||||
} else {
|
|
||||||
formatted.append('Z');
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatted.toString();
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
* @param pos The position to start parsing from, updated to where parsing stopped.
|
|
||||||
* @return the parsed date
|
|
||||||
* @throws ParseException if the date is not in the appropriate format
|
|
||||||
*/
|
|
||||||
private static Date parse(String date, ParsePosition pos) throws ParseException {
|
|
||||||
Exception fail = null;
|
|
||||||
try {
|
|
||||||
int offset = pos.getIndex();
|
|
||||||
|
|
||||||
// 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 (checkOffset(date, offset, 'T')) {
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
// milliseconds can be optional in the format
|
|
||||||
if (checkOffset(date, offset, '.')) {
|
|
||||||
milliseconds = parseInt(date, offset += 1, offset += 3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// extract timezone
|
|
||||||
String timezoneId;
|
|
||||||
if (date.length() <= offset) {
|
|
||||||
throw new IllegalArgumentException("No time zone indicator");
|
|
||||||
}
|
|
||||||
char timezoneIndicator = date.charAt(offset);
|
|
||||||
if (timezoneIndicator == '+' || timezoneIndicator == '-') {
|
|
||||||
String timezoneOffset = date.substring(offset);
|
|
||||||
timezoneId = GMT_ID + timezoneOffset;
|
|
||||||
offset += timezoneOffset.length();
|
|
||||||
} else if (timezoneIndicator == 'Z') {
|
|
||||||
timezoneId = GMT_ID;
|
|
||||||
offset += 1;
|
|
||||||
} else {
|
|
||||||
throw new IndexOutOfBoundsException("Invalid time zone indicator " + timezoneIndicator);
|
|
||||||
}
|
|
||||||
|
|
||||||
TimeZone timezone = TimeZone.getTimeZone(timezoneId);
|
|
||||||
if (!timezone.getID().equals(timezoneId)) {
|
|
||||||
throw new IndexOutOfBoundsException();
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
pos.setIndex(offset);
|
|
||||||
return calendar.getTime();
|
|
||||||
// If we get a ParseException it'll already have the right message/offset.
|
|
||||||
// Other exception types can convert here.
|
|
||||||
} catch (IndexOutOfBoundsException e) {
|
|
||||||
fail = e;
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
fail = e;
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
fail = e;
|
|
||||||
}
|
|
||||||
String input = (date == null) ? null : ("'" + date + "'");
|
|
||||||
throw new ParseException("Failed to parse date [" + input + "]: " + fail.getMessage(), pos.getIndex());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
result = -digit;
|
|
||||||
}
|
|
||||||
while (i < endIndex) {
|
|
||||||
digit = Character.digit(value.charAt(i++), 10);
|
|
||||||
if (digit < 0) {
|
|
||||||
throw new NumberFormatException("Invalid number: " + value);
|
|
||||||
}
|
|
||||||
result *= 10;
|
|
||||||
result -= digit;
|
|
||||||
}
|
|
||||||
return -result;
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue