/*! ****************************************************************************** * * Pentaho Data Integration * * Copyright (C) 2002-2013 by Pentaho : http://www.pentaho.com * ******************************************************************************* * * 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 org.pentaho.di.core.row.value.timestamp; import java.lang.reflect.Method; import java.sql.Timestamp; import java.text.AttributedCharacterIterator; import java.text.DateFormatSymbols; import java.text.DecimalFormat; import java.text.FieldPosition; import java.text.ParseException; import java.text.ParsePosition; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; /** * User: Dzmitry Stsiapanau Date: 3/13/14 Time: 6:32 PM */ public class SimpleTimestampFormat extends SimpleDateFormat { private static final long serialVersionUID = -848077738238548608L; /** * Cached nanosecond positions in specified pattern. */ private int startNanosecondPatternPosition; private int endNanosecondPatternPosition; /** * Flag noticed that specified pattern can be succesfully operated by parent <code>SimpleDateFormat</code> */ private boolean compatibleToSuperPattern = true; /** * The pattern string of this formatter. This is always a non-localized pattern. May not be null. See parent class * documentation for details. * * @serial */ private String originalPattern; /** * Cached nanoseconds formatter. */ private DecimalFormat nanoseconds; /** * Localized nanosecond letter. */ private char patternNanosecond; /** * Letter which is used to specify in pattern nanoseconds component. Was extended from parent * <code>SimpleDateFormat</code> millisecond, so it is still could be internationalized. */ private static final int PATTERN_MILLISECOND_POSITION = 8; // S /** * Internal <code>SimpleDateFormat</code> instances are used in formatting. */ private static final String DEFAULT_TIMESTAMP_FORMAT_FOR_TIMESTAMP = "yyyy-MM-dd HH:mm:ss"; private static final String DEFAULT_MILLISECOND_DATE_FORMAT = "SSS"; private static final SimpleDateFormat defaultTimestampFormat = new SimpleDateFormat( DEFAULT_TIMESTAMP_FORMAT_FOR_TIMESTAMP, Locale.US ); private static final SimpleDateFormat defaultMillisecondDateFormat = new SimpleDateFormat( DEFAULT_MILLISECOND_DATE_FORMAT, Locale.US ); /** * Nanoseconds placeholder to specify unformatted nanoseconds position after formatting <code>Date</code> part part of * the <code>Timestamp</code>. */ private static final String NANOSECOND_PLACEHOLDER = "NANO"; private static final char FORMATTER_ESCAPE_CHARACTER = '\''; private static final String ESCAPED_NANOSECOND_PLACEHOLDER = FORMATTER_ESCAPE_CHARACTER + "NANO" + FORMATTER_ESCAPE_CHARACTER; /** * Default format of the <code>Timestamp</code> object for sql. */ public static final String DEFAULT_TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm:ss.SSSSSSSSS"; /** * Fields for advantages of using locale version from JRE 1.7 and for JRE 1.6 compatibility */ private static Method getDefaultLocaleMethod; private static Class<?> localeCategoryClass; private static Object formatCategory; private static boolean formatCategoryLocaleAvailable = true; static { try { localeCategoryClass = Class.forName( "java.util.Locale$Category" ); final Class<?> localeClass = Class.forName( "java.util.Locale" ); final Class<?>[] paramTypes = new Class<?>[] { localeCategoryClass }; getDefaultLocaleMethod = localeClass.getMethod( "getDefault", paramTypes ); final java.lang.reflect.Field formatField = localeCategoryClass.getField( "FORMAT" ); //we pass null because the FORMAT is an enumeration constant(the same applies for class variables) formatCategory = formatField.get( null ); } catch ( Exception e ) { formatCategoryLocaleAvailable = false; } } /** * Sets the date and time format symbols of this date format. * * @param newFormatSymbols the new date and time format symbols * @throws NullPointerException if the given newFormatSymbols is null * @see #getDateFormatSymbols */ @Override public void setDateFormatSymbols( DateFormatSymbols newFormatSymbols ) { patternNanosecond = newFormatSymbols.getLocalPatternChars().charAt( PATTERN_MILLISECOND_POSITION ); super.setDateFormatSymbols( newFormatSymbols ); } private void init( String pattern, DateFormatSymbols formatSymbols, Boolean compiledPattern ) { originalPattern = pattern; String datePattern = pattern; super.setDateFormatSymbols( formatSymbols ); patternNanosecond = formatSymbols.getLocalPatternChars().charAt( PATTERN_MILLISECOND_POSITION ); StringBuilder sb = new StringBuilder(); startNanosecondPatternPosition = datePattern.indexOf( patternNanosecond ); endNanosecondPatternPosition = datePattern.lastIndexOf( patternNanosecond ); initNanosecondsFormat(); if ( startNanosecondPatternPosition != -1 ) { sb.append( datePattern.substring( 0, startNanosecondPatternPosition ) ); sb.append( FORMATTER_ESCAPE_CHARACTER ); sb.append( NANOSECOND_PLACEHOLDER ); sb.append( FORMATTER_ESCAPE_CHARACTER ); sb.append( datePattern.substring( endNanosecondPatternPosition + 1 ) ); datePattern = sb.toString(); sb.setLength( 0 ); } String patternToApply; if ( startNanosecondPatternPosition == -1 || endNanosecondPatternPosition - startNanosecondPatternPosition < 3 ) { compatibleToSuperPattern = true; patternToApply = originalPattern; } else { compatibleToSuperPattern = false; patternToApply = datePattern; } if ( compiledPattern ) { super.applyLocalizedPattern( patternToApply ); } else { super.applyPattern( patternToApply ); } } /** * Constructs a <code>SimpleTimestampFormat</code> using the given pattern and the default date format symbols for the * default locale. <b>Note:</b> This constructor may not support all locales. For full coverage, use the factory * methods in the {@link SimpleTimestampFormat} class. * * @param pattern the pattern describing the date and time format * @throws NullPointerException if the given pattern is null * @throws IllegalArgumentException if the given pattern is invalid */ public SimpleTimestampFormat( String pattern ) { this( pattern, getCompatibleLocale() ); } private static Locale getCompatibleLocale() { Locale locale = null; if ( formatCategoryLocaleAvailable ) { try { locale = (Locale) getDefaultLocaleMethod.invoke( localeCategoryClass, formatCategory ); } catch ( Exception ignored ) { //ignored } } //for jre 6 if ( locale == null ) { locale = Locale.getDefault(); } return locale; } /** * Constructs a <code>SimpleTimestampFormat</code> using the given pattern and the default date format symbols for the * given locale. <b>Note:</b> This constructor may not support all locales. For full coverage, use the factory methods * in the {@link SimpleTimestampFormat} class. * * @param pattern the pattern describing the date and time format * @param locale the locale whose date format symbols should be used * @throws NullPointerException if the given pattern or locale is null * @throws IllegalArgumentException if the given pattern is invalid */ public SimpleTimestampFormat( String pattern, Locale locale ) { this( pattern, DateFormatSymbols.getInstance( locale ) ); } /** * Constructs a <codeSimpleTimestampFormat</code> using the given pattern and date format symbols. * * @param pattern the pattern describing the date and time format * @param formatSymbols the date format symbols to be used for formatting * @throws NullPointerException if the given pattern or formatSymbols is null * @throws IllegalArgumentException if the given pattern is invalid */ public SimpleTimestampFormat( String pattern, DateFormatSymbols formatSymbols ) { super( pattern, formatSymbols ); init( pattern, formatSymbols, false ); } /** * Formats the given <code>Date</code> or <code></>Timestamp</code> into a date/time string and appends the result to * the given <code>StringBuffer</code>. * * @param timestamp the date-time value to be formatted into a date-time string. * @param toAppendTo where the new date-time text is to be appended. * @param pos the formatting position. On input: an alignment field, if desired. On output: the offsets of the * alignment field. * @return the formatted date-time string. * @throws NullPointerException if the given {@code timestamp} is {@code null}. */ @Override public StringBuffer format( Date timestamp, StringBuffer toAppendTo, FieldPosition pos ) { if ( compatibleToSuperPattern ) { return super.format( timestamp, toAppendTo, pos ); } StringBuffer dateBuffer; String nan; if ( timestamp instanceof Timestamp ) { Timestamp tmp = (Timestamp) timestamp; Date date = new Date( tmp.getTime() ); dateBuffer = super.format( date, toAppendTo, pos ); nan = formatNanoseconds( tmp.getNanos() ); } else { dateBuffer = super.format( timestamp, toAppendTo, pos ); String milliseconds = defaultMillisecondDateFormat.format( timestamp ); nan = formatNanoseconds( Integer.valueOf( milliseconds ) * Math.pow( 10, 6 ) ); } int placeholderPosition = replaceHolder( dateBuffer, false ); return dateBuffer.insert( pos.getBeginIndex() + placeholderPosition, nan ); } private String formatNanoseconds( Double v ) { return formatNanoseconds( v.intValue() ); } private String formatNanoseconds( Integer nanos ) { String nan = nanoseconds.format( nanos ); return nan.substring( 0, endNanosecondPatternPosition - startNanosecondPatternPosition + 1 ); } private void initNanosecondsFormat() { StringBuilder nanos = new StringBuilder(); for ( int i = startNanosecondPatternPosition; i <= endNanosecondPatternPosition; i++ ) { nanos.append( '0' ); } nanoseconds = new DecimalFormat( nanos.toString() ); } private int replaceHolder( StringBuffer dateBuffer, Boolean inPattern ) { String placeHolder = inPattern ? ESCAPED_NANOSECOND_PLACEHOLDER : NANOSECOND_PLACEHOLDER; int placeholderPosition = dateBuffer.indexOf( placeHolder ); if ( placeholderPosition == -1 ) { return 0; } dateBuffer.delete( placeholderPosition, placeholderPosition + placeHolder.length() ); return placeholderPosition; } /** * See <code>SimpleDateFormat</code> description. This is dummy method to deprecate using parent implementation for * <code>Timestamp</code> until it is not fully implemented. */ @Override public AttributedCharacterIterator formatToCharacterIterator( Object obj ) { if ( obj instanceof Timestamp ) { throw new IllegalArgumentException( "This functionality for Timestamp object has not been implemented yet" ); } if ( compatibleToSuperPattern ) { return super.formatToCharacterIterator( obj ); } else { throw new IllegalArgumentException( "This functionality for specified format pattern has not been implemented yet" ); } } /** * Parses text from a string to produce a <code>Timestamp</code>. * <p/> * The method attempts to parse text starting at the index given by <code>pos</code>. If parsing succeeds, then the * index of <code>pos</code> is updated to the index after the last character used (parsing does not necessarily use * all characters up to the end of the string), and the parsed date is returned. The updated <code>pos</code> can be * used to indicate the starting point for the next call to this method. If an error occurs, then the index of * <code>pos</code> is not changed, the error index of <code>pos</code> is set to the index of the character where the * error occurred, and null is returned. * <p/> * <p>This parsing operation uses the {@link SimpleDateFormat#calendar calendar} to produce a {@code Date}. All of the * {@code calendar}'s date-time fields are {@linkplain java.util.Calendar#clear() cleared} before parsing, and the * {@code calendar}'s default values of the date-time fields are used for any missing date-time information. For * example, the year value of the parsed {@code Date} is 1970 with {@link java.util.GregorianCalendar} if no year * value is given from the parsing operation. The {@code TimeZone} value may be overwritten, depending on the given * pattern and the time zone value in {@code text}. Any {@code TimeZone} value that has previously been set by a call * to {@link #setTimeZone(java.util.TimeZone) setTimeZone} may need to be restored for further operations. * * @param text A <code>String</code>, part of which should be parsed. * @param pos A <code>ParsePosition</code> object with index and error index information as described above. * @return A <code>Date</code> parsed from the string. In case of error, returns null. * @throws NullPointerException if <code>text</code> or <code>pos</code> is null. */ @Override public Date parse( String text, ParsePosition pos ) { String timestampFormatDate; Date tempDate; if ( compatibleToSuperPattern ) { tempDate = super.parse( text, pos ); return new Timestamp( tempDate.getTime() ); } StringBuilder dateText = new StringBuilder( text.substring( pos.getIndex() ) ); ParsePosition positionError = new ParsePosition( 0 ); tempDate = super.parse( dateText.toString(), positionError ); if ( tempDate != null ) { pos.setErrorIndex( pos.getIndex() ); return null; } int startNanosecondsPosition = positionError.getErrorIndex(); int endNanosecondsPosition = endNanosecondPatternPosition - startNanosecondPatternPosition + 1 + startNanosecondsPosition; endNanosecondsPosition = ( endNanosecondsPosition >= dateText.length() ) ? dateText.length() : endNanosecondsPosition; String nanoseconds = String.valueOf( dateText.subSequence( startNanosecondsPosition, endNanosecondsPosition ) ); dateText.delete( startNanosecondsPosition, endNanosecondsPosition ); ParsePosition position = new ParsePosition( 0 ); dateText.append( NANOSECOND_PLACEHOLDER ); tempDate = super.parse( dateText.toString(), position ); if ( tempDate == null ) { pos.setErrorIndex( position.getErrorIndex() ); return null; } timestampFormatDate = defaultTimestampFormat.format( tempDate ); String result = timestampFormatDate + '.' + nanoseconds; Timestamp res = Timestamp.valueOf( timestampFormatDate + '.' + nanoseconds ); pos.setIndex( pos.getIndex() + result.length() ); return res; } /** * Returns a pattern string describing this date format. * * @return a pattern string describing this date format. */ @Override public String toPattern() { return originalPattern; } /** * Returns a localized pattern string describing this date format. * * @return a localized pattern string describing this date format. */ @Override public String toLocalizedPattern() { if ( compatibleToSuperPattern ) { return super.toLocalizedPattern(); } else { StringBuffer pattern = new StringBuffer( super.toLocalizedPattern() ); int placeholderPosition = replaceHolder( pattern, true ); for ( int i = placeholderPosition; i <= endNanosecondPatternPosition - startNanosecondPatternPosition + placeholderPosition; i++ ) { pattern.insert( i, patternNanosecond ); } return pattern.toString(); } } /** * Applies the given pattern string to this date format. * * @param pattern the new date and time pattern for this date format * @throws NullPointerException if the given pattern is null * @throws IllegalArgumentException if the given pattern is invalid */ @Override public void applyPattern( String pattern ) { DateFormatSymbols formatSymbols = super.getDateFormatSymbols(); init( pattern, formatSymbols, false ); } /** * Applies the given localized pattern string to this date format. * * @param pattern a String to be mapped to the new date and time format pattern for this format * @throws NullPointerException if the given pattern is null * @throws IllegalArgumentException if the given pattern is invalid */ @Override public void applyLocalizedPattern( String pattern ) { DateFormatSymbols formatSymbols = super.getDateFormatSymbols(); init( pattern, formatSymbols, true ); } /** * Parses text from the beginning of the given string to produce a date. The method may not use the entire text of the * given string. * <p/> * See the {@link #parse(String, java.text.ParsePosition)} method for more information on date parsing. * * @param source A <code>String</code> whose beginning should be parsed. * @return A <code>Date</code> parsed from the string. * @throws java.text.ParseException if the beginning of the specified string cannot be parsed. */ @Override public Date parse( String source ) throws ParseException { return super.parse( source ); } /** * Parses text from a string to produce a <code>Date</code>. * <p/> * The method attempts to parse text starting at the index given by <code>pos</code>. If parsing succeeds, then the * index of <code>pos</code> is updated to the index after the last character used (parsing does not necessarily use * all characters up to the end of the string), and the parsed date is returned. The updated <code>pos</code> can be * used to indicate the starting point for the next call to this method. If an error occurs, then the index of * <code>pos</code> is not changed, the error index of <code>pos</code> is set to the index of the character where the * error occurred, and null is returned. * <p/> * See the {@link #parse(String, java.text.ParsePosition)} method for more information on date parsing. * * @param source A <code>String</code>, part of which should be parsed. * @param pos A <code>ParsePosition</code> object with index and error index information as described above. * @return A <code>Date</code> parsed from the string. In case of error, returns null. * @throws NullPointerException if <code>pos</code> is null. */ @Override public Object parseObject( String source, ParsePosition pos ) { return parse( source, pos ); } }