blob: 847908792a05e2502143303200f2079941bb0d9c [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.directory.shared.ldap.util;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Calendar;
import java.util.TimeZone;
/**
* <p>This class represents the generalized time syntax as defined in
* RFC 4517 section 3.3.13.</p>
*
* <p>The date, time and time zone information is internally backed
* by an {@link java.util.Calendar} object</p>
*
* <p>Leap seconds are not supported, as {@link java.util.Calendar}
* does not support leap seconds.</p>
*
* <pre>
* 3.3.13. Generalized Time
*
* A value of the Generalized Time syntax is a character string
* representing a date and time. The LDAP-specific encoding of a value
* of this syntax is a restriction of the format defined in [ISO8601],
* and is described by the following ABNF:
*
* GeneralizedTime = century year month day hour
* [ minute [ second / leap-second ] ]
* [ fraction ]
* g-time-zone
*
* century = 2(%x30-39) ; "00" to "99"
* year = 2(%x30-39) ; "00" to "99"
* month = ( %x30 %x31-39 ) ; "01" (January) to "09"
* / ( %x31 %x30-32 ) ; "10" to "12"
* day = ( %x30 %x31-39 ) ; "01" to "09"
* / ( %x31-32 %x30-39 ) ; "10" to "29"
* / ( %x33 %x30-31 ) ; "30" to "31"
* hour = ( %x30-31 %x30-39 ) / ( %x32 %x30-33 ) ; "00" to "23"
* minute = %x30-35 %x30-39 ; "00" to "59"
*
* second = ( %x30-35 %x30-39 ) ; "00" to "59"
* leap-second = ( %x36 %x30 ) ; "60"
*
* fraction = ( DOT / COMMA ) 1*(%x30-39)
* g-time-zone = %x5A ; "Z"
* / g-differential
* g-differential = ( MINUS / PLUS ) hour [ minute ]
* MINUS = %x2D ; minus sign ("-")
*
* The <DOT>, <COMMA>, and <PLUS> rules are defined in [RFC4512].
*
* The above ABNF allows character strings that do not represent valid
* dates (in the Gregorian calendar) and/or valid times (e.g., February
* 31, 1994). Such character strings SHOULD be considered invalid for
* this syntax.
*
* The time value represents coordinated universal time (equivalent to
* Greenwich Mean Time) if the "Z" form of <g-time-zone> is used;
* otherwise, the value represents a local time in the time zone
* indicated by <g-differential>. In the latter case, coordinated
* universal time can be calculated by subtracting the differential from
* the local time. The "Z" form of <g-time-zone> SHOULD be used in
* preference to <g-differential>.
*
* If <minute> is omitted, then <fraction> represents a fraction of an
* hour; otherwise, if <second> and <leap-second> are omitted, then
* <fraction> represents a fraction of a minute; otherwise, <fraction>
* represents a fraction of a second.
*
* Examples:
* 199412161032Z
* 199412160532-0500
*
* Both example values represent the same coordinated universal time:
* 10:32 AM, December 16, 1994.
*
* The LDAP definition for the Generalized Time syntax is:
*
* ( 1.3.6.1.4.1.1466.115.121.1.24 DESC 'Generalized Time' )
*
* This syntax corresponds to the GeneralizedTime ASN.1 type from
* [ASN.1], with the constraint that local time without a differential
* SHALL NOT be used.
*
* </pre>
*/
public class GeneralizedTime implements Comparable<GeneralizedTime>
{
public enum Format
{
YEAR_MONTH_DAY_HOUR_MIN_SEC, YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION,
YEAR_MONTH_DAY_HOUR_MIN, YEAR_MONTH_DAY_HOUR_MIN_FRACTION,
YEAR_MONTH_DAY_HOUR, YEAR_MONTH_DAY_HOUR_FRACTION, ;
}
public enum FractionDelimiter
{
DOT, COMMA
}
public enum TimeZoneFormat
{
Z, DIFF_HOUR, DIFF_HOUR_MINUTE;
}
private static final TimeZone GMT = TimeZone.getTimeZone( "GMT" );
/** The user provided value */
private String upGeneralizedTime;
/** The user provided format */
private Format upFormat;
/** The user provided time zone format */
private TimeZoneFormat upTimeZoneFormat;
/** The user provided fraction delimiter */
private FractionDelimiter upFractionDelimiter;
/** the user provided fraction length */
private int upFractionLength;
/** The calendar */
private Calendar calendar;
/**
* Creates a new instance of GeneralizedTime, based on the given Calendar object.
* Uses <pre>Format.YEAR_MONTH_DAY_HOUR_MIN_SEC</pre> as default format and
* <pre>TimeZoneFormat.Z</pre> as default time zone format.
*
* @param calendar the calendar containing the date, time and timezone information
*/
public GeneralizedTime( Calendar calendar )
{
if ( calendar == null )
{
throw new IllegalArgumentException( "Calendar must not be null." );
}
this.calendar = calendar;
upGeneralizedTime = null;
upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_SEC;
upTimeZoneFormat = TimeZoneFormat.Z;
upFractionDelimiter = FractionDelimiter.DOT;
upFractionLength = 3;
}
/**
* Creates a new instance of GeneralizedTime, based on the
* given generalized time string.
*
* @param generalizedTime the generalized time
*
* @throws ParseException if the given generalized time can't be parsed.
*/
public GeneralizedTime( String generalizedTime ) throws ParseException
{
if ( generalizedTime == null )
{
throw new ParseException( "generalizedTime is null", 0 );
}
this.upGeneralizedTime = generalizedTime;
calendar = Calendar.getInstance();
calendar.setTimeInMillis( 0 );
calendar.setLenient( false );
parseYear();
parseMonth();
parseDay();
parseHour();
if ( upGeneralizedTime.length() < 11 )
{
throw new ParseException(
"Generalized Time too short, doesn't contain field 'minute' or 'fraction of hour' or 'timezone'.", 10 );
}
// pos 10:
// if digit => minute field
// if . or , => fraction of hour field
// if Z or + or - => timezone field
// else error
int pos = 10;
char c = upGeneralizedTime.charAt( pos );
if ( '0' <= c && c <= '9' )
{
parseMinute();
if ( upGeneralizedTime.length() < 13 )
{
throw new ParseException(
"Generalized Time too short, doesn't contain field 'second' or 'fraction of minute' or 'timezone'.",
12 );
}
// pos 12:
// if digit => second field
// if . or , => fraction of minute field
// if Z or + or - => timezone field
// else error
pos = 12;
c = upGeneralizedTime.charAt( pos );
if ( '0' <= c && c <= '9' )
{
parseSecond();
if ( upGeneralizedTime.length() < 15 )
{
throw new ParseException(
"Generalized Time too short, doesn't contain field 'fraction of second' or 'timezone'.", 14 );
}
// pos 14:
// if . or , => fraction of second field
// if Z or + or - => timezone field
// else error
pos = 14;
c = upGeneralizedTime.charAt( pos );
if ( c == '.' || c == ',' )
{
// read fraction of second
parseFractionOfSecond();
pos += 1 + upFractionLength;
parseTimezone( pos );
upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION;
}
else if ( c == 'Z' || c == '+' || c == '-' )
{
// read timezone
parseTimezone( pos );
upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_SEC;
}
else
{
throw new ParseException(
"Invalid Time too short, expected field 'fraction of second' or 'timezone'.", 14 );
}
}
else if ( c == '.' || c == ',' )
{
// read fraction of minute
parseFractionOfMinute();
pos += 1 + upFractionLength;
parseTimezone( pos );
upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN_FRACTION;
}
else if ( c == 'Z' || c == '+' || c == '-' )
{
// read timezone
parseTimezone( pos );
upFormat = Format.YEAR_MONTH_DAY_HOUR_MIN;
}
else
{
throw new ParseException(
"Invalid Time too short, expected field 'second' or 'fraction of minute' or 'timezone'.", 12 );
}
}
else if ( c == '.' || c == ',' )
{
// read fraction of hour
parseFractionOfHour();
pos += 1 + upFractionLength;
parseTimezone( pos );
upFormat = Format.YEAR_MONTH_DAY_HOUR_FRACTION;
}
else if ( c == 'Z' || c == '+' || c == '-' )
{
// read timezone
parseTimezone( pos );
upFormat = Format.YEAR_MONTH_DAY_HOUR;
}
else
{
throw new ParseException(
"Invalid Generalized Time, expected field 'minute' or 'fraction of hour' or 'timezone'.", 10 );
}
// this calculates and verifies the calendar
try
{
calendar.getTimeInMillis();
}
catch ( IllegalArgumentException iae )
{
throw new ParseException( "Invalid date/time values.", 0 );
}
}
private void parseTimezone( int pos ) throws ParseException
{
if ( upGeneralizedTime.length() < pos + 1 )
{
throw new ParseException( "Generalized Time too short, doesn't contain field 'timezone'.", pos );
}
char c = upGeneralizedTime.charAt( pos );
if ( c == 'Z' )
{
calendar.setTimeZone( GMT );
upTimeZoneFormat = TimeZoneFormat.Z;
if ( upGeneralizedTime.length() > pos + 1 )
{
throw new ParseException( "Invalid Generalized Time, expected 'timezone' as the last field.", pos + 1 );
}
}
else if ( c == '+' || c == '-' )
{
StringBuilder sb = new StringBuilder( "GMT" );
sb.append( c );
String digits = getAllDigits( pos + 1 );
sb.append( digits );
if ( digits.length() == 2 && digits.matches( "^([01]\\d|2[0-3])$" ) )
{
TimeZone timeZone = TimeZone.getTimeZone( sb.toString() );
calendar.setTimeZone( timeZone );
upTimeZoneFormat = TimeZoneFormat.DIFF_HOUR;
}
else if ( digits.length() == 4 && digits.matches( "^([01]\\d|2[0-3])([0-5]\\d)$" ) )
{
TimeZone timeZone = TimeZone.getTimeZone( sb.toString() );
calendar.setTimeZone( timeZone );
upTimeZoneFormat = TimeZoneFormat.DIFF_HOUR_MINUTE;
}
else
{
throw new ParseException(
"Invalid Generalized Time, expected field 'timezone' must contain 2 or 4 digits.", pos );
}
if ( upGeneralizedTime.length() > pos + 1 + digits.length() )
{
throw new ParseException( "Invalid Generalized Time, expected 'timezone' as the last field.", pos + 1
+ digits.length() );
}
}
}
private void parseFractionOfSecond() throws ParseException
{
parseFractionDelmiter( 14 );
String fraction = getFraction( 14 + 1 );
upFractionLength = fraction.length();
double fract = Double.parseDouble( "0." + fraction );
int millisecond = ( int ) Math.round( fract * 1000 );
calendar.set( Calendar.MILLISECOND, millisecond );
}
private void parseFractionOfMinute() throws ParseException
{
parseFractionDelmiter( 12 );
String fraction = getFraction( 12 + 1 );
upFractionLength = fraction.length();
double fract = Double.parseDouble( "0." + fraction );
int milliseconds = ( int ) Math.round( fract * 1000 * 60 );
int second = milliseconds / 1000;
int millisecond = milliseconds - ( second * 1000 );
calendar.set( Calendar.SECOND, second );
calendar.set( Calendar.MILLISECOND, millisecond );
}
private void parseFractionOfHour() throws ParseException
{
parseFractionDelmiter( 10 );
String fraction = getFraction( 10 + 1 );
upFractionLength = fraction.length();
double fract = Double.parseDouble( "0." + fraction );
int milliseconds = ( int ) Math.round( fract * 1000 * 60 * 60 );
int minute = milliseconds / ( 1000 * 60 );
int second = ( milliseconds - ( minute * 60 * 1000 ) ) / 1000;
int millisecond = milliseconds - ( minute * 60 * 1000 ) - ( second * 1000 );
calendar.set( Calendar.MINUTE, minute );
calendar.set( Calendar.SECOND, second );
calendar.set( Calendar.MILLISECOND, millisecond );
}
private void parseFractionDelmiter( int fractionDelimiterPos )
{
char c = upGeneralizedTime.charAt( fractionDelimiterPos );
upFractionDelimiter = c == '.' ? FractionDelimiter.DOT : FractionDelimiter.COMMA;
}
private String getFraction( int startIndex ) throws ParseException
{
String fraction = getAllDigits( startIndex );
// minimum one digit
if ( fraction.length() == 0 )
{
throw new ParseException( "Generalized Time too short, doesn't contain number for 'fraction'.", startIndex );
}
return fraction;
}
private String getAllDigits( int startIndex )
{
StringBuilder sb = new StringBuilder();
while ( upGeneralizedTime.length() > startIndex )
{
char c = upGeneralizedTime.charAt( startIndex );
if ( '0' <= c && c <= '9' )
{
sb.append( c );
startIndex++;
}
else
{
break;
}
}
return sb.toString();
}
private void parseSecond() throws ParseException
{
// read minute
if ( upGeneralizedTime.length() < 14 )
{
throw new ParseException( "Generalized Time too short, doesn't contain field 'second'.", 12 );
}
try
{
int second = Integer.parseInt( upGeneralizedTime.substring( 12, 14 ) );
calendar.set( Calendar.SECOND, second );
}
catch ( NumberFormatException e )
{
throw new ParseException( "Invalid Generalized Time, field 'second' is not numeric.", 12 );
}
}
private void parseMinute() throws ParseException
{
// read minute
if ( upGeneralizedTime.length() < 12 )
{
throw new ParseException( "Generalized Time too short, doesn't contain field 'minute'.", 10 );
}
try
{
int minute = Integer.parseInt( upGeneralizedTime.substring( 10, 12 ) );
calendar.set( Calendar.MINUTE, minute );
}
catch ( NumberFormatException e )
{
throw new ParseException( "Invalid Generalized Time, field 'minute' is not numeric.", 10 );
}
}
private void parseHour() throws ParseException
{
if ( upGeneralizedTime.length() < 10 )
{
throw new ParseException( "Generalized Time too short, doesn't contain field 'hour'.", 8 );
}
try
{
int hour = Integer.parseInt( upGeneralizedTime.substring( 8, 10 ) );
calendar.set( Calendar.HOUR_OF_DAY, hour );
}
catch ( NumberFormatException e )
{
throw new ParseException( "Invalid Generalized Time, field 'hour' is not numeric.", 8 );
}
}
private void parseDay() throws ParseException
{
if ( upGeneralizedTime.length() < 8 )
{
throw new ParseException( "Generalized Time too short, doesn't contain field 'day'.", 6 );
}
try
{
int day = Integer.parseInt( upGeneralizedTime.substring( 6, 8 ) );
calendar.set( Calendar.DAY_OF_MONTH, day );
}
catch ( NumberFormatException e )
{
throw new ParseException( "Invalid Generalized Time, field 'day' is not numeric.", 6 );
}
}
private void parseMonth() throws ParseException
{
if ( upGeneralizedTime.length() < 6 )
{
throw new ParseException( "Generalized Time too short, doesn't contain field 'month'.", 4 );
}
try
{
int month = Integer.parseInt( upGeneralizedTime.substring( 4, 6 ) );
calendar.set( Calendar.MONTH, month - 1 );
}
catch ( NumberFormatException e )
{
throw new ParseException( "Invalid Generalized Time, field 'month' is not numeric.", 4 );
}
}
private void parseYear() throws ParseException
{
if ( upGeneralizedTime.length() < 4 )
{
throw new ParseException( "Generalized Time too short, doesn't contain field 'century/year'.", 0 );
}
try
{
int year = Integer.parseInt( upGeneralizedTime.substring( 0, 4 ) );
calendar.set( Calendar.YEAR, year );
}
catch ( NumberFormatException e )
{
throw new ParseException( "Invalid Generalized Time, field 'century/year' is not numeric.", 0 );
}
}
/**
* Returns the string representation of this generalized time.
* This method uses the same format as the user provided format.
*
* @return the string representation of this generalized time
*/
public String toGeneralizedTime()
{
return toGeneralizedTime( upFormat, upFractionDelimiter, upFractionLength, upTimeZoneFormat );
}
/**
* Returns the string representation of this generalized time.
*
* @param format the target format
* @param fractionDelimiter the target fraction delimiter, may be null
* @param fractionLenth the fraction length
* @param timeZoneFormat the target time zone format
*
* @return the string
*/
public String toGeneralizedTime( Format format, FractionDelimiter fractionDelimiter, int fractionLength,
TimeZoneFormat timeZoneFormat )
{
NumberFormat twoDigits = new DecimalFormat( "00" );
NumberFormat fourDigits = new DecimalFormat( "00" );
String fractionFormat = "";
for ( int i = 0; i < fractionLength && i < 3; i++ )
{
fractionFormat += "0";
}
StringBuilder sb = new StringBuilder();
sb.append( fourDigits.format( calendar.get( Calendar.YEAR ) ) );
sb.append( twoDigits.format( calendar.get( Calendar.MONTH ) + 1 ) );
sb.append( twoDigits.format( calendar.get( Calendar.DAY_OF_MONTH ) ) );
sb.append( twoDigits.format( calendar.get( Calendar.HOUR_OF_DAY ) ) );
switch ( format )
{
case YEAR_MONTH_DAY_HOUR_MIN_SEC:
sb.append( twoDigits.format( calendar.get( Calendar.MINUTE ) ) );
sb.append( twoDigits.format( calendar.get( Calendar.SECOND ) ) );
break;
case YEAR_MONTH_DAY_HOUR_MIN_SEC_FRACTION:
sb.append( twoDigits.format( calendar.get( Calendar.MINUTE ) ) );
sb.append( twoDigits.format( calendar.get( Calendar.SECOND ) ) );
NumberFormat fractionDigits = new DecimalFormat( fractionFormat );
sb.append( fractionDelimiter == FractionDelimiter.COMMA ? ',' : '.' );
sb.append( fractionDigits.format( calendar.get( Calendar.MILLISECOND ) ) );
break;
case YEAR_MONTH_DAY_HOUR_MIN:
sb.append( twoDigits.format( calendar.get( Calendar.MINUTE ) ) );
break;
case YEAR_MONTH_DAY_HOUR_MIN_FRACTION:
sb.append( twoDigits.format( calendar.get( Calendar.MINUTE ) ) );
// sec + millis => fraction of minute
double millisec = 1000 * calendar.get( Calendar.SECOND ) + calendar.get( Calendar.MILLISECOND );
double fraction = millisec / ( 1000 * 60 );
fractionDigits = new DecimalFormat( "0." + fractionFormat );
sb.append( fractionDelimiter == FractionDelimiter.COMMA ? ',' : '.' );
sb.append( fractionDigits.format( fraction ).substring( 2 ) );
break;
case YEAR_MONTH_DAY_HOUR_FRACTION:
// min + sec + millis => fraction of minute
millisec = 1000 * 60 * calendar.get( Calendar.MINUTE ) + 1000 * calendar.get( Calendar.SECOND )
+ calendar.get( Calendar.MILLISECOND );
fraction = millisec / ( 1000 * 60 * 60 );
fractionDigits = new DecimalFormat( "0." + fractionFormat );
sb.append( fractionDelimiter == FractionDelimiter.COMMA ? ',' : '.' );
sb.append( fractionDigits.format( fraction ).substring( 2 ) );
break;
}
if ( timeZoneFormat == TimeZoneFormat.Z && calendar.getTimeZone().hasSameRules( GMT ) )
{
sb.append( 'Z' );
}
else
{
TimeZone timeZone = calendar.getTimeZone();
int rawOffset = timeZone.getRawOffset();
sb.append( rawOffset < 0 ? '-' : '+' );
rawOffset = Math.abs( rawOffset );
int hour = rawOffset / ( 60 * 60 * 1000 );
int minute = ( rawOffset - ( hour * 60 * 60 * 1000 ) ) / ( 1000 * 60 );
if ( hour < 10 )
{
sb.append( '0' );
}
sb.append( hour );
if ( timeZoneFormat == TimeZoneFormat.DIFF_HOUR_MINUTE || timeZoneFormat == TimeZoneFormat.Z )
{
if ( hour < 10 )
{
sb.append( '0' );
}
sb.append( minute );
}
}
return sb.toString();
}
/**
* Gets the calendar. It could be used to manipulate this
* {@link GeneralizedTime} settings.
*
* @return the calendar
*/
public Calendar getCalendar()
{
return calendar;
}
@Override
public String toString()
{
return toGeneralizedTime();
}
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + calendar.hashCode();
return result;
}
@Override
public boolean equals( Object obj )
{
if ( obj instanceof GeneralizedTime )
{
GeneralizedTime other = ( GeneralizedTime ) obj;
return calendar.equals( other.calendar );
}
else
{
return false;
}
}
/**
* Compares this GeneralizedTime object with the specified GeneralizedTime object.
*
* @param other the other GeneralizedTime object
*
* @return a negative integer, zero, or a positive integer as this object
* is less than, equal to, or greater than the specified object.
*
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
public int compareTo( GeneralizedTime other )
{
return calendar.compareTo( other.calendar );
}
}