/*
* Redistribution and use of this software and associated documentation
* ("Software"), with or without modification, are permitted provided
* that the following conditions are met:
*
* 1. Redistributions of source code must retain copyright
* statements and notices. Redistributions must also contain a
* copy of this document.
*
* 2. Redistributions in binary form must reproduce the
* above copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. The name "Exolab" must not be used to endorse or promote
* products derived from this Software without prior written
* permission of Intalio, Inc. For written permission,
* please contact info@exolab.org.
*
* 4. Products derived from this Software may not be called "Exolab"
* nor may "Exolab" appear in their names without prior written
* permission of Intalio, Inc. Exolab is a registered
* trademark of Intalio, Inc.
*
* 5. Due credit should be given to the Exolab Project
* (http://www.exolab.org/).
*
* THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
* NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* INTALIO, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Copyright 2000 (C) Intalio, Inc. All Rights Reserved.
*
* $Id$
* Date Author Changes
* 05/13/2001 Arnaud Blandin Added the support for omitted components (section 4.5 of ISO8601)
* 12/05/2000 Arnaud Blandin Added the support for NotSupportedOperationException
* 11/08/2000 Arnaud Blandin Added new constructor and setValues method
* 11/07/2000 Arnaud Blandin Added isEqual() and isGreater() methods
* 11/02/2000 Arnaud Blandin Changed the constructor
* 10/26/2000 Arnaud Blandin Created
*/
package org.exolab.castor.types;
import java.util.Date;
import java.util.SimpleTimeZone;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import org.exolab.castor.xml.ValidationException;
/**
* Represents recurringDuration utterly a recurringDuration must contain all the
* fields:
* <p>
* (+|-)CCYY-MM-DDThh:mm:ss.sss(Z|(+|-)hh:mm)
* <p>
* The validation of the date fields is done in the set methods and follows <a
* href="http://www.iso.ch/markete/8601.pdf">the ISO8601 Date and Time Format</a>
* <p>
* It is possible to omit higher components by using '-'.
* <p>
* Note: This datatype is not included in any recommendation. It was introduced
* in http://www.w3.org/TR/1999/WD-xmlschema-2-19990924/ and was last in
* http://www.w3.org/TR/2000/CR-xmlschema-2-20001024/ and was removed by
* http://www.w3.org/TR/2001/PR-xmlschema-2-20010316/. It was not in the final
* approved recommendation: http://www.w3.org/TR/2001/REC-xmlschema-2-20010502/
*
* @author <a href="mailto:blandin@intalio.com">Arnaud Blandin</a>
* @version $Revision$
* @deprecated since Castor 1.0.6 since this type is not in any recommendation.
*/
public class RecurringDuration extends RecurringDurationBase{
/** SerialVersionUID */
private static final long serialVersionUID = -6037158412155942249L;
/** The date format used by the toDate() method */
private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS";
/** Set to true and recompile to include debugging code in class. */
private static final boolean DEBUG = false;
//Private variables
//-1 means that the field has been omitted (cf section 4.5 of ISO 8601)
private short _century = 0;
private short _year = 0;
private short _month = 0;
private short _day = 0;
private static final short OMITED = Short.parseShort("-1");
public RecurringDuration() {
}
/**
* returns a recurringDuration with the facets
* duration and period set up
* @param duration the TimeDuration representing the duration facet
* @param period the TimeDuration reprensenting the period facet
*/
public RecurringDuration(TimeDuration duration, TimeDuration period) {
super(duration,period);
}
/**
* returns a recurringDuration with the facets
* duration and period set up
* @param duration the String representing the duration facet
* @param period the String reprensenting the period facet
*/
public RecurringDuration(String duration, String period) {
super(duration, period);
}
/**
* returns a recurringDuration with the facets
* duration and period set up but also the fields
* @param duration the String representing the duration facet
* @param period the String reprensenting the period facet
* @param values an array of shorts which contains the values of the fields
* @see #setValues
*/
public RecurringDuration(String duration, String period, short[] values)
throws UnsupportedOperationException
{
this(duration, period);
if (values.length != 10) {
throw new IllegalArgumentException("Wrong numbers of values");
}
this.setValues(values);
}
/**
* set the century field
* @param century the value to set up
*/
public void setCentury(short century) {
String err ="";
if (century < -1) {
err = "century : "+century+" must not be a negative value.";
throw new IllegalArgumentException(err);
}
_century = century;
}
/**
* set the Year field
* Note: 0000 is not allowed
* @param year year to set up
*/
public void setYear(short year)
throws UnsupportedOperationException
{
String err ="";
if (year < -1) {
err = "year : "+year+" must not be a negative value.";
throw new IllegalArgumentException(err);
}
else if ( (year == -1) && (_century != -1) ) {
err = "year can not be omitted if century is not omitted.";
throw new IllegalArgumentException(err);
}
else if ( (year ==0) && (_century==0)) {
err = "0000 is not an allowed year";
throw new IllegalArgumentException(err);
}
_year = year;
}
/**
* set the Month Field
* @param month the value to set up
* Note 1<month<12
*/
public void setMonth(short month)
throws UnsupportedOperationException
{
String err ="";
if (month == -1) {
if (_century != -1) {
err = "month cannot be omitted if the previous component is not omitted.\n"+
"only higher level components can be omitted.";
throw new IllegalArgumentException(err);
}
}
else if (month < 1) {
err = "month : "+month+" is not a correct value."
+"\n 1<month<12";
throw new IllegalArgumentException(err);
}
else if (month > 12) {
err = "month : "+month+" is not a correct value.";
err+= "\n 1<month<12";
throw new IllegalArgumentException(err);
}
_month = month;
}
/**
* set the Day Field
* @param day the value to set up
* Note a validation is done on the day field
*/
public void setDay(short day)
throws UnsupportedOperationException
{
String err = "";
if (day == -1) {
if (_month != -1) {
err = "day cannot be omitted if the previous component is not omitted.\n"+
"only higher level components can be omitted.";
throw new IllegalArgumentException(err);
}
}
else if (day < 1) {
err = "day : "+day+" is not a correct value.";
err+= "\n 1<day";
throw new IllegalArgumentException(err);
}
// in february
if (_month == 2) {
if (isLeap()) {
if (day > 29) {
err = "day : "+day+" is not a correct value.";
err+= "\n day<30 (leap year and month is february)";
throw new IllegalArgumentException(err);
}
} else if (day > 28) {
err = "day : "+day+" is not a correct value.";
err+= "\n day<30 (not a leap year and month is february)";
throw new IllegalArgumentException(err);
} //february
} else if ( (_month == 4) || (_month == 6) ||
(_month == 9) || (_month == 11) )
{
if (day > 30) {
err = "day : "+day+" is not a correct value.";
err+= "\n day<31 ";
throw new IllegalArgumentException(err);
}
} else if (day > 31) {
err = "day : "+day+" is not a correct value.";
err+= "\n day<=31 ";
throw new IllegalArgumentException(err);
}
_day = day;
}
/**
* return true if the year field represents a leap year
* A specific year is a leap year if it is either evenly
* divisible by 400 OR evenly divisible by 4 and not evenly divisible by 100
* @return true if the year field represents a leap year
*/
public boolean isLeap () {
int temp = (_century * 100 + _year) ;
boolean result =( ((temp % 4) == 0) && ((temp % 100) != 0) );
result = (result || ((temp % 400)==0) );
return result;
}
/**
* set all the fields by reading the values in an array
* @param values an array of shorts with the values
* the array is supposed to be of length 10 and ordered like that:
*<ul>
* <li>century</li>
* <li>year</li>
* <li>month</li>
* <li>day</li>
* <li>hour</li>
* <li>minute</li>
* <li>second</li>
* <li>millisecond</li>
* <li>zoneHour</li>
* <li>zoneMinute</li>
* </ul>
*
* @throws UnsupportedOperationException this exception is thrown if changing
* the value of one field os not allowed
* @see RecurringDurationBase#setValues
*/
public void setValues(short[] values)
throws UnsupportedOperationException
{
this.setCentury(values[0]);
this.setYear(values[1]);
this.setMonth(values[2]);
this.setDay(values[3]);
this.setHour(values[4]);
this.setMinute(values[5]);
this.setSecond(values[6],values[7]);
this.setZone(values[8],values[9]);
}
//Get methods
public short getCentury() {
return(_century);
}
public short getYear() {
return(_year);
}
public short getMonth() {
return(_month);
}
public short getDay() {
return(_day);
}
/**
* returns an array of short with all the fields which describe
* a RecurringDuration
* @return an array of short with all the fields which describe
* a RecurringDuration
*/
public short[] getValues() {
short[] result = null;
result = new short[10];
result[0] = this.getCentury();
result[1] = this.getYear();
result[2] = this.getMonth();
result[3] = this.getDay();
result[4] = this.getHour();
result[5] = this.getMinute();
result[6] = this.getSeconds();
result[7] = this.getMilli();
result[8] = this.getZoneHour();
result[5] = this.getZoneMinute();
return result;
} //getValues
/**
* convert this recurringDuration into a local Date
* <p>Note : Be aware a the 'local' property of the date i.e <tt>toDate()</tt> will de the
* conversion between a UTC date and your computer date format.
* For instance if you have set up your computer time zone on the Pacific Day Time
* the conversion of <tt>2000-10-20T00:00:00.000</tt> into a <tt>java.util.Date</tt>
* will return <tt>Thu Oct 19 17:00:00 PDT 2000</tt>
* @return a local date representing this recurringDuration
* @throws ParseException
*/
public Date toDate() throws ParseException {
Date date = null;
SimpleDateFormat df = new SimpleDateFormat(DATE_FORMAT);
SimpleTimeZone timeZone = new SimpleTimeZone(0,"UTC");
// Set the time zone
if ( !isUTC() ) {
int offset = 0;
offset = ( (this.getZoneMinute() + this.getZoneHour()*60)*60*1000);
offset = isZoneNegative() ? -offset : offset;
timeZone.setRawOffset(offset);
timeZone.setID(TimeZone.getAvailableIDs(offset)[0]);
}
df.setTimeZone(timeZone);
date = df.parse(this.toPrivateString());
return date;
}//toDate()
/**
* <p>Convert this recurringDuration to a string
* <p>The format is defined by W3C XML Schema draft and ISO8601
* i.e (+|-)CCYY-MM-DDThh:mm:ss.sss(Z|(+|-)hh:mm)
* @return a string representing this recurringDuration
*/
public String toString() {
return this.toPrivateString();
}
/*This method is needed for the toDate() method
*/
private final String toPrivateString() {
StringBuffer result = new StringBuffer();
StringBuffer timeZone = null;
if (this.getCentury() == -1)
result.append('-');
else {
if (this.getCentury()/10 == 0)
result.append(0);
result.append(this.getCentury());
if ((this.getYear()/10) == 0)
result.append(0);
result.append(this.getYear());
}
result.append('-');
if (this.getMonth() == -1)
result.append('-');
else {
if ((this.getMonth() / 10) == 0 )
result.append(0);
result.append(this.getMonth());
}
result.append('-');
if (this.getDay() == -1)
result.append('-');
else {
if ((this.getDay()/10) == 0 )
result.append(0);
result.append(this.getDay());
}
// nowhere it is said in the specs that Time can be omitted
// choose to always keep it
result.append("T");
if (this.getHour() == -1)
result.append('-');
else {
if ((this.getHour()/10) == 0)
result.append(0);
result.append(this.getHour());
}
result.append(':');
if (this.getMinute() == -1)
result.append('-');
else {
if ((this.getMinute() / 10) == 0 )
result.append(0);
result.append(this.getMinute());
}
result.append(':');
if (this.getSeconds() == -1)
result.append('-');
else {
if ((this.getSeconds()/10) == 0 )
result.append(0);
result.append(this.getSeconds());
}
result.append('.');
result.append(this.getMilli());
if (isNegative())
result.append('-');
// by default we choose to not concat the Z
if (!isUTC()) {
timeZone = new StringBuffer();
if ((this.getZoneHour()/10) == 0)
timeZone.append(0);
timeZone.append(this.getZoneHour());
timeZone.append(':');
if ((this.getZoneMinute()/10) == 0)
timeZone.append(0);
timeZone.append(this.getZoneMinute());
if (isZoneNegative())
timeZone.insert(0,'-');
else timeZone.insert(0,'+');
result.append(timeZone.toString());
}
if (isNegative())
result.insert(0,'-');
return result.toString();
}//toString
public static Object parse(String str) throws ParseException {
return parseRecurringDuration(str);
}
/**
* Parse a String and convert it into a recurringDuration.
*
* @param str The string to parse.
* @return The recurringDuration represented by the string.
* @throws ParseException A parse exception is thrown if the string to parse
* does not follow the rigth format (see the description of this class).
*/
public static RecurringDuration parseRecurringDuration(String str) throws ParseException {
//TODO optimize this method (too much strings)
RecurringDuration result = new RecurringDuration();
//remove if necessary the Z at the end
if ( str.endsWith("Z"))
str = str.substring(0,str.indexOf("Z"));
//isNegative ?
if ( str.startsWith("-") && !str.startsWith("--"))
result.setNegative();
//Is there a time Zone?
String zoneStr = str.substring(str.length()-6,str.length());
boolean timeZone = ( ((zoneStr.lastIndexOf("-") !=-1) ||
(zoneStr.lastIndexOf("+") !=-1 )) &&
(zoneStr.lastIndexOf(":") !=-1) );
if (DEBUG) {
System.out.println("In parsing method of RecurringDuration");
System.out.println("String to parse : "+str);
System.out.println("Negative ? "+result.isNegative());
String tzone = timeZone?zoneStr:"false";
System.out.println("Time zone :" +tzone);
}
if (!timeZone) zoneStr = null;
else {
int index = str.lastIndexOf("+") != -1? str.lastIndexOf("+") :
str.lastIndexOf("-");
str = str.substring(0,index);
}
// the 'T' is required
if (str.indexOf('T') == -1) {
throw new ParseException("The 'T' element is required",0);
}
String date = str.substring(0,str.indexOf("T"));
String time = str.substring(str.indexOf("T"));
// proceed date
StringTokenizer token = new StringTokenizer(date,"-");
if (token.countTokens() > 3)
throw new ParseException(str+": Bad date format",0);
try {
//CCYY
boolean process = false;
String temp;
if (token.countTokens() == 3) {
temp = token.nextToken();
if (temp.length() != 4)
throw new ParseException(str+":Bad year format",1);
if (DEBUG) {
System.out.println("Processing century: "+temp.substring(0,2));
}
result.setCentury(Short.parseShort( temp.substring(0,2) ));
if (DEBUG) {
System.out.println("Processing year: "+temp.substring(2,4));
}
result.setYear(Short.parseShort( temp.substring(2,4) ));
process = true;
}
if (!process)
result.setCentury(OMITED);
if (token.countTokens() == 2) {
//MM
temp=token.nextToken();
if (temp.length() != 2)
throw new ParseException(str+": Bad month format",5);
if (DEBUG) {
System.out.println("Processing month: "+temp);
}
result.setMonth(Short.parseShort(temp));
process = true;
}
if ((!process)) {
result.setMonth(OMITED);
}
if (token.countTokens() == 1) {
//DD
temp=token.nextToken();
if (temp.length() != 2)
throw new ParseException(str+":Bad day format",8);
if (DEBUG) {
System.out.println("Processing day: "+temp);
}
result.setDay(Short.parseShort(temp));
process = true;
}
if (!process) {
result.setDay(OMITED);
}
//proceed Time
token = new StringTokenizer(time,":");
process = false;
if (token.countTokens() > 5)
throw new ParseException(str+": Bad time format",11);
if (token.countTokens() == 3) {
//hh
temp = token.nextToken();
temp = temp.substring(temp.indexOf("T")+1);
if (temp.length() != 2)
throw new ParseException(str+": Bad hour format",11);
if (DEBUG) {
System.out.println("Processing hour: "+temp);
}
result.setHour(Short.parseShort( temp ));
process = true;
}
if (!process) {
if (result.getDay() == OMITED )
result.setHour(OMITED);
else throw new IllegalArgumentException("hour cannot be omitted");
}
if (token.countTokens() == 2) {
//mm
temp=token.nextToken();
if (temp.length() != 2)
throw new ParseException(str+": Bad minute format",14);
if (DEBUG) {
System.out.println("Processing minute: "+temp);
}
result.setMinute( Short.parseShort(temp));
process = true;
}
if (!process){
if (result.getDay() == OMITED ) {
result.setHour(OMITED);
result.setMinute(OMITED);
}
else throw new IllegalArgumentException("hour cannot be omitted");
}
if (token.countTokens() == 1) {
//ss
temp=token.nextToken();
String milsecond = "0";
if (temp.indexOf(".") != -1) {
milsecond = temp.substring(temp.indexOf(".")+1);
temp = temp.substring(0,temp.indexOf("."));
}
if (temp.length() != 2)
throw new ParseException(str+": Bad second format",17);
if (DEBUG) {
System.out.println("Processing seconds: "+temp);
}
result.setSecond(Short.parseShort(temp.substring(0,2)),
Short.parseShort(milsecond));
process = true;
}
if (!process) {
if (result.getDay() == OMITED ) {
result.setHour(OMITED);
result.setMinute(OMITED);
result.setSecond(OMITED,OMITED);
}
else throw new IllegalArgumentException("hour cannot be omitted");
}
// proceed TimeZone if any
if (timeZone) {
if (zoneStr.startsWith("-")) result.setZoneNegative();
if (zoneStr.length()!= 6)
throw new ParseException(str+": Bad time zone format",20);
result.setZone(Short.parseShort(zoneStr.substring(1,3)),
Short.parseShort(zoneStr.substring(4,6)));
}
else result.isUTC();
temp = null;
} catch (UnsupportedOperationException e) {
//we are sure that this method is used with a recurringDuration
//(if not a ParseException is thrown) so we can never reach that point
}
return result;
}//parse
/**
* Override the java.lang.equals method
* @see #equal
*/
public boolean equals(Object object) {
if (object instanceof RecurringDuration) {
try {
return equal( (RecurringDuration) object);
} catch (ValidationException e) {
e.printStackTrace();
return false;
}
}
return false;
}
/**
* <p> Returns true if the present instance of Recurring Duration is equal to
* the parameter.
* <p>The equals relation is the following :
* <p> <tt>rd1 equals rd2 </tt> iff each field of rd1 is equal to the corresponding field of rd2
* @param reccD the recurring duration to compare with the present instance
* @return true if the present instance is equal to the parameter false if not
*/
public boolean equal(RecurringDuration reccD) throws ValidationException
{
boolean result = false;
if (reccD == null)
return false;
if ( !(this.getPeriod().equals(reccD.getPeriod())) ||
!(this.getDuration().equals(reccD.getDuration())) )
{
String err = " Recurring Duration which have different values "
+"for the duration and period can not be compared";
throw new ValidationException(err);
}
result = this.getCentury() == reccD.getCentury();
result = result && (this.getYear() == reccD.getYear());
result = result && (this.getMonth() == reccD.getMonth());
result = result && (this.getDay() == reccD.getDay());
result = result && (this.getHour() == reccD.getHour());
result = result && (this.getMinute() == reccD.getMinute());
result = result && (this.getSeconds() == reccD.getSeconds());
result = result && (this.getMilli() == reccD.getMilli());
result = result && (this.isNegative() == this.isNegative());
if (!reccD.isUTC()) {
result = result && (!this.isUTC());
result = result && (this.getZoneHour() == reccD.getZoneHour());
result = result && (this.getZoneMinute() == reccD.getZoneMinute());
}
return result;
}//equals
/**
* <p>Returns true if the present instance of RecurringDuration is greater than
* the parameter
* <p>Note : the order relation follows the W3C XML Schema draft i.e
* <tt>rd1 < rd2 </tt>iff rd2-rd1>0
* @param reccD the recurring duration to compare with the present instance
* @return true if the present instance is the greatest, false if not
*/
public boolean isGreater(RecurringDuration reccD) throws ValidationException
{
boolean result = false;
if ( !(this.getPeriod().equals(reccD.getPeriod())) ||
!(this.getDuration().equals(reccD.getDuration())) )
{
String err = " Recurring Duration which have different values "
+"for the duration and period can not be compared";
throw new ValidationException(err);
}
short[] val_this = this.getValues();
short[] val_reccD = reccD.getValues();
int i = 0;
while ( (result != true) && (i< (val_this.length-1)) ) {
result = val_this[i] > val_reccD[i];
if ( val_this[i] < val_reccD[i])
return false;
i++;
}
return result;
}//isGreater
} //RecurringDuration