All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.fitbur.jackson.databind.util.StdDateFormat Maven / Gradle / Ivy

There is a newer version: 1.0.0
Show newest version
package com.fitbur.jackson.databind.util;

import java.text.DateFormat;
import java.text.FieldPosition;
import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.*;

import com.fitbur.jackson.core.io.NumberInput;

/**
 * Default {@link DateFormat} implementation used by standard Date
 * serializers and deserializers. For serialization defaults to using
 * an ISO-8601 compliant format (format String "yyyy-MM-dd'T'HH:mm:ss.SSSZ")
 * and for deserialization, both ISO-8601 and RFC-1123.
 */
@SuppressWarnings("serial")
public class StdDateFormat
    extends DateFormat
{
    /* TODO !!! 24-Nov-2009, tatu: Need to rewrite this class:
     * JDK date parsing is awfully brittle, and ISO-8601 is quite
     * permissive. The two don't mix, need to write a better one.
     */
    // 02-Oct-2014, tatu: Alas. While spit'n'polished a few times, still
    //   not really robust. But still in use.

    /**
     * Defines a commonly used date format that conforms
     * to ISO-8601 date formatting standard, when it includes basic undecorated
     * timezone definition
     */
    public final static String DATE_FORMAT_STR_ISO8601 = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";

    /**
     * Same as 'regular' 8601, but handles 'Z' as an alias for "+0000"
     * (or "UTC")
     */
    protected final static String DATE_FORMAT_STR_ISO8601_Z = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";

    /**
     * ISO-8601 with just the Date part, no time
     */
    protected final static String DATE_FORMAT_STR_PLAIN = "yyyy-MM-dd";

    /**
     * This constant defines the date format specified by
     * RFC 1123 / RFC 822.
     */
    protected final static String DATE_FORMAT_STR_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";

    /**
     * For error messages we'll also need a list of all formats.
     */
    protected final static String[] ALL_FORMATS = new String[] {
        DATE_FORMAT_STR_ISO8601,
        DATE_FORMAT_STR_ISO8601_Z,
        DATE_FORMAT_STR_RFC1123,
        DATE_FORMAT_STR_PLAIN
    };

    /**
     * By default we use UTC for everything, with Jackson 2.7 and later
     * (2.6 and earlier relied on GMT)
     */
    private final static TimeZone DEFAULT_TIMEZONE;
    static {
        DEFAULT_TIMEZONE = TimeZone.getTimeZone("UTC"); // since 2.7
    }

    private final static Locale DEFAULT_LOCALE = Locale.US;
    
    protected final static DateFormat DATE_FORMAT_RFC1123;

    protected final static DateFormat DATE_FORMAT_ISO8601;
    protected final static DateFormat DATE_FORMAT_ISO8601_Z;

    protected final static DateFormat DATE_FORMAT_PLAIN;

    /* Let's construct "blueprint" date format instances: can not be used
     * as is, due to thread-safety issues, but can be used for constructing
     * actual instances more cheaply (avoids re-parsing).
     */
    static {
        /* Another important thing: let's force use of default timezone for
         * baseline DataFormat objects
         */

        DATE_FORMAT_RFC1123 = new SimpleDateFormat(DATE_FORMAT_STR_RFC1123, DEFAULT_LOCALE);
        DATE_FORMAT_RFC1123.setTimeZone(DEFAULT_TIMEZONE);
        DATE_FORMAT_ISO8601 = new SimpleDateFormat(DATE_FORMAT_STR_ISO8601, DEFAULT_LOCALE);
        DATE_FORMAT_ISO8601.setTimeZone(DEFAULT_TIMEZONE);
        DATE_FORMAT_ISO8601_Z = new SimpleDateFormat(DATE_FORMAT_STR_ISO8601_Z, DEFAULT_LOCALE);
        DATE_FORMAT_ISO8601_Z.setTimeZone(DEFAULT_TIMEZONE);
        DATE_FORMAT_PLAIN = new SimpleDateFormat(DATE_FORMAT_STR_PLAIN, DEFAULT_LOCALE);
        DATE_FORMAT_PLAIN.setTimeZone(DEFAULT_TIMEZONE);
    }
    
    /**
     * A singleton instance can be used for cloning purposes, as a blueprint of sorts.
     */
    public final static StdDateFormat instance = new StdDateFormat();
    
    /**
     * Caller may want to explicitly override timezone to use; if so,
     * we will have non-null value here.
     */
    protected transient TimeZone _timezone;

    protected final Locale _locale;

    /**
     * Explicit override for leniency, if specified.
     *

* Can not be `final` because {@link #setLenient(boolean)} returns * `void`. * * @since 2.7 */ protected Boolean _lenient; protected transient DateFormat _formatRFC1123; protected transient DateFormat _formatISO8601; protected transient DateFormat _formatISO8601_z; protected transient DateFormat _formatPlain; /* /********************************************************** /* Life cycle, accessing singleton "standard" formats /********************************************************** */ public StdDateFormat() { _locale = DEFAULT_LOCALE; } @Deprecated // since 2.7 public StdDateFormat(TimeZone tz, Locale loc) { _timezone = tz; _locale = loc; } protected StdDateFormat(TimeZone tz, Locale loc, Boolean lenient) { _timezone = tz; _locale = loc; _lenient = lenient; } public static TimeZone getDefaultTimeZone() { return DEFAULT_TIMEZONE; } /** * Method used for creating a new instance with specified timezone; * if no timezone specified, defaults to the default timezone (UTC). */ public StdDateFormat withTimeZone(TimeZone tz) { if (tz == null) { tz = DEFAULT_TIMEZONE; } if ((tz == _timezone) || tz.equals(_timezone)) { return this; } return new StdDateFormat(tz, _locale, _lenient); } public StdDateFormat withLocale(Locale loc) { if (loc.equals(_locale)) { return this; } return new StdDateFormat(_timezone, loc, _lenient); } @Override public StdDateFormat clone() { /* Although there is that much state to share, we do need to * orchestrate a bit, mostly since timezones may be changed */ return new StdDateFormat(_timezone, _locale, _lenient); } /** * @deprecated Since 2.4; use variant that takes Locale */ @Deprecated public static DateFormat getISO8601Format(TimeZone tz) { return getISO8601Format(tz, DEFAULT_LOCALE); } /** * Method for getting a non-shared DateFormat instance * that uses specified timezone and can handle simple ISO-8601 * compliant date format. * * @since 2.4 */ public static DateFormat getISO8601Format(TimeZone tz, Locale loc) { return _cloneFormat(DATE_FORMAT_ISO8601, DATE_FORMAT_STR_ISO8601, tz, loc, null); } /** * Method for getting a non-shared DateFormat instance * that uses specific timezone and can handle RFC-1123 * compliant date format. * * @since 2.4 */ public static DateFormat getRFC1123Format(TimeZone tz, Locale loc) { return _cloneFormat(DATE_FORMAT_RFC1123, DATE_FORMAT_STR_RFC1123, tz, loc, null); } /** * @deprecated Since 2.4; use variant that takes Locale */ @Deprecated public static DateFormat getRFC1123Format(TimeZone tz) { return getRFC1123Format(tz, DEFAULT_LOCALE); } /* /********************************************************** /* Public API, configuration /********************************************************** */ @Override // since 2.6 public TimeZone getTimeZone() { return _timezone; } @Override public void setTimeZone(TimeZone tz) { /* DateFormats are timezone-specific (via Calendar contained), * so need to reset instances if timezone changes: */ if (!tz.equals(_timezone)) { _clearFormats(); _timezone = tz; } } /** * Need to override since we need to keep track of leniency locally, * and not via underlying {@link Calendar} instance like base class * does. */ @Override // since 2.7 public void setLenient(boolean enabled) { Boolean newValue = enabled; if (_lenient != newValue) { _lenient = newValue; // and since leniency settings may have been used: _clearFormats(); } } @Override // since 2.7 public boolean isLenient() { if (_lenient == null) { // default is, I believe, true return true; } return _lenient.booleanValue(); } /* /********************************************************** /* Public API, parsing /********************************************************** */ @Override public Date parse(String dateStr) throws ParseException { dateStr = dateStr.trim(); ParsePosition pos = new ParsePosition(0); Date dt; if (looksLikeISO8601(dateStr)) { // also includes "plain" dt = parseAsISO8601(dateStr, pos, true); } else { // Also consider "stringified" simple time stamp int i = dateStr.length(); while (--i >= 0) { char ch = dateStr.charAt(i); if (ch < '0' || ch > '9') { // 07-Aug-2013, tatu: And [databind#267] points out that negative numbers should also work if (i > 0 || ch != '-') { break; } } } if ((i < 0) // let's just assume negative numbers are fine (can't be RFC-1123 anyway); check length for positive && (dateStr.charAt(0) == '-' || NumberInput.inLongRange(dateStr, false))) { dt = new Date(Long.parseLong(dateStr)); } else { // Otherwise, fall back to using RFC 1123 dt = parseAsRFC1123(dateStr, pos); } } if (dt != null) { return dt; } StringBuilder sb = new StringBuilder(); for (String f : ALL_FORMATS) { if (sb.length() > 0) { sb.append("\", \""); } else { sb.append('"'); } sb.append(f); } sb.append('"'); throw new ParseException (String.format("Can not parse date \"%s\": not compatible with any of standard forms (%s)", dateStr, sb.toString()), pos.getErrorIndex()); } @Override public Date parse(String dateStr, ParsePosition pos) { if (looksLikeISO8601(dateStr)) { // also includes "plain" try { return parseAsISO8601(dateStr, pos, false); } catch (ParseException e) { // will NOT be thrown due to false but is declared... return null; } } // Also consider "stringified" simple time stamp int i = dateStr.length(); while (--i >= 0) { char ch = dateStr.charAt(i); if (ch < '0' || ch > '9') { // 07-Aug-2013, tatu: And [databind#267] points out that negative numbers should also work if (i > 0 || ch != '-') { break; } } } if (i < 0) { // all digits // let's just assume negative numbers are fine (can't be RFC-1123 anyway); check length for positive if (dateStr.charAt(0) == '-' || NumberInput.inLongRange(dateStr, false)) { return new Date(Long.parseLong(dateStr)); } } // Otherwise, fall back to using RFC 1123 return parseAsRFC1123(dateStr, pos); } /* /********************************************************** /* Public API, writing /********************************************************** */ @Override public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) { if (_formatISO8601 == null) { _formatISO8601 = _cloneFormat(DATE_FORMAT_ISO8601, DATE_FORMAT_STR_ISO8601, _timezone, _locale, _lenient); } return _formatISO8601.format(date, toAppendTo, fieldPosition); } /* /********************************************************** /* Std overrides /********************************************************** */ @Override public String toString() { String str = "DateFormat "+getClass().getName(); TimeZone tz = _timezone; if (tz != null) { str += " (timezone: "+tz+")"; } str += "(locale: "+_locale+")"; return str; } /* /********************************************************** /* Helper methods /********************************************************** */ /** * Overridable helper method used to figure out which of supported * formats is the likeliest match. */ protected boolean looksLikeISO8601(String dateStr) { if (dateStr.length() >= 5 && Character.isDigit(dateStr.charAt(0)) && Character.isDigit(dateStr.charAt(3)) && dateStr.charAt(4) == '-' ) { return true; } return false; } protected Date parseAsISO8601(String dateStr, ParsePosition pos, boolean throwErrors) throws ParseException { /* 21-May-2009, tatu: DateFormat has very strict handling of * timezone modifiers for ISO-8601. So we need to do some scrubbing. */ /* First: do we have "zulu" format ('Z' == "UTC")? If yes, that's * quite simple because we already set date format timezone to be * UTC, and hence can just strip out 'Z' altogether */ int len = dateStr.length(); char c = dateStr.charAt(len-1); DateFormat df; String formatStr; // Need to support "plain" date... if (len <= 10 && Character.isDigit(c)) { df = _formatPlain; formatStr = DATE_FORMAT_STR_PLAIN; if (df == null) { df = _formatPlain = _cloneFormat(DATE_FORMAT_PLAIN, formatStr, _timezone, _locale, _lenient); } } else if (c == 'Z') { df = _formatISO8601_z; formatStr = DATE_FORMAT_STR_ISO8601_Z; if (df == null) { df = _formatISO8601_z = _cloneFormat(DATE_FORMAT_ISO8601_Z, formatStr, _timezone, _locale, _lenient); } // may be missing milliseconds... if so, add if (dateStr.charAt(len-4) == ':') { StringBuilder sb = new StringBuilder(dateStr); sb.insert(len-1, ".000"); dateStr = sb.toString(); } } else { // Let's see if we have timezone indicator or not... if (hasTimeZone(dateStr)) { c = dateStr.charAt(len-3); if (c == ':') { // remove optional colon // remove colon StringBuilder sb = new StringBuilder(dateStr); sb.delete(len-3, len-2); dateStr = sb.toString(); } else if (c == '+' || c == '-') { // missing minutes // let's just append '00' dateStr += "00"; } // Milliseconds partial or missing; and even seconds are optional len = dateStr.length(); // remove 'T', '+'/'-' and 4-digit timezone-offset int timeLen = len - dateStr.lastIndexOf('T') - 6; if (timeLen < 12) { // 8 for hh:mm:ss, 4 for .sss int offset = len - 5; // insertion offset, before tz-offset StringBuilder sb = new StringBuilder(dateStr); switch (timeLen) { case 11: sb.insert(offset, '0'); break; case 10: sb.insert(offset, "00"); break; case 9: // is this legal? (just second fraction marker) sb.insert(offset, "000"); break; case 8: sb.insert(offset, ".000"); break; case 7: // not legal to have single-digit second break; case 6: // probably not legal, but let's allow sb.insert(offset, "00.000"); case 5: // is legal to omit seconds sb.insert(offset, ":00.000"); } dateStr = sb.toString(); } df = _formatISO8601; formatStr = DATE_FORMAT_STR_ISO8601; if (_formatISO8601 == null) { df = _formatISO8601 = _cloneFormat(DATE_FORMAT_ISO8601, formatStr, _timezone, _locale, _lenient); } } else { // If not, plain date. Easiest to just patch 'Z' in the end? StringBuilder sb = new StringBuilder(dateStr); // And possible also millisecond part if missing int timeLen = len - dateStr.lastIndexOf('T') - 1; if (timeLen < 12) { // missing, or partial switch (timeLen) { case 11: sb.append('0'); case 10: sb.append('0'); case 9: sb.append('0'); break; default: sb.append(".000"); } } sb.append('Z'); dateStr = sb.toString(); df = _formatISO8601_z; formatStr = DATE_FORMAT_STR_ISO8601_Z; if (df == null) { df = _formatISO8601_z = _cloneFormat(DATE_FORMAT_ISO8601_Z, formatStr, _timezone, _locale, _lenient); } } } Date dt = df.parse(dateStr, pos); // 22-Dec-2015, tatu: With non-lenient, may get null if (dt == null) { throw new ParseException (String.format("Can not parse date \"%s\": while it seems to fit format '%s', parsing fails (leniency? %s)", dateStr, formatStr, _lenient), pos.getErrorIndex()); } return dt; } protected Date parseAsRFC1123(String dateStr, ParsePosition pos) { if (_formatRFC1123 == null) { _formatRFC1123 = _cloneFormat(DATE_FORMAT_RFC1123, DATE_FORMAT_STR_RFC1123, _timezone, _locale, _lenient); } return _formatRFC1123.parse(dateStr, pos); } private final static boolean hasTimeZone(String str) { // Only accept "+hh", "+hhmm" and "+hh:mm" (and with minus), so int len = str.length(); if (len >= 6) { char c = str.charAt(len-6); if (c == '+' || c == '-') return true; c = str.charAt(len-5); if (c == '+' || c == '-') return true; c = str.charAt(len-3); if (c == '+' || c == '-') return true; } return false; } private final static DateFormat _cloneFormat(DateFormat df, String format, TimeZone tz, Locale loc, Boolean lenient) { if (!loc.equals(DEFAULT_LOCALE)) { df = new SimpleDateFormat(format, loc); df.setTimeZone((tz == null) ? DEFAULT_TIMEZONE : tz); } else { df = (DateFormat) df.clone(); if (tz != null) { df.setTimeZone(tz); } } if (lenient != null) { df.setLenient(lenient.booleanValue()); } return df; } protected void _clearFormats() { _formatRFC1123 = null; _formatISO8601 = null; _formatISO8601_z = null; _formatPlain = null; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy