/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-2007 The eXist Project
* http://exist-db.org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* $Id$
*/
package org.exist.xquery.value;
import java.math.BigDecimal;
import java.text.Collator;
import java.text.DecimalFormat;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.datatype.DatatypeConstants;
import javax.xml.datatype.Duration;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.namespace.QName;
import org.exist.xquery.Constants;
import org.exist.xquery.XPathException;
/**
* @author wolf
* @author <a href="mailto:piotr@ideanest.com">Piotr Kaminski</a>
* @author ljo
*/
public abstract class AbstractDateTimeValue extends ComputableValue {
//Provisionally public
public final XMLGregorianCalendar calendar;
private XMLGregorianCalendar implicitCalendar, canonicalCalendar, trimmedCalendar;
protected static Pattern negativeDateStart = Pattern.compile("^\\d\\d?-(\\d+)-(.*)");
protected static Pattern gYearWUTCTZ = Pattern.compile("^(\\d\\d\\d\\d)([+-])(\\d\\d):(\\d\\d)$");
protected static Pattern gYearNoTZ = Pattern.compile("^(\\d\\d\\d\\d)$");
protected static Pattern gDayWTZ = Pattern.compile("^(---\\d\\d)([+-])(\\d\\d):(\\d\\d)");
protected static Pattern gMonthWTZ = Pattern.compile("^(--\\d\\d)([+-])(\\d\\d):(\\d\\d)");
//protected static Pattern gMonthWUTCTZ = Pattern.compile("^(--\\d\\d)([+-])(\\d\\d):(\\d\\d)");
protected static Pattern gYearMonthWTZ = Pattern.compile("^(\\d\\d\\d\\d-\\d\\d)([+-])(\\d\\d):(\\d\\d)");
protected static Pattern gMonthDayWTZ = Pattern.compile("^(--\\d\\d-\\d\\d)([+-])(\\d\\d):(\\d\\d)");
protected static Pattern dateWTZ = Pattern.compile("^(\\d\\d\\d\\d-\\d\\d-\\d\\d)([+-])(\\d\\d):(\\d\\d)");
protected static Pattern timeNoTZ = Pattern.compile("^(\\d\\d):(\\d\\d):(\\d\\d)");
protected static Pattern timeWUTCTZ = Pattern.compile("^(\\d\\d):(\\d\\d):(\\d\\d)([+-]\\d\\d:\\d\\d)");
protected static Pattern timeMsWTZ = Pattern.compile("^(\\d\\d):(\\d\\d):(\\d\\d)(\\.)(\\d\\d\\d)([+-])(\\d\\d):(\\d\\d)");
protected static Pattern dateTimeMsWTZ = Pattern.compile("^(\\d\\d\\d\\d-\\d\\d-\\d\\dT)(\\d\\d):(\\d\\d):(\\d\\d)(\\.)(\\d\\d\\d)([+-])(\\d\\d):(\\d\\d)");
protected static Pattern dateTimeNoTZ = Pattern.compile("^(\\d\\d\\d\\d-\\d\\d-\\d\\d)(T)(\\d\\d):(\\d\\d):(\\d\\d)");
protected static Pattern dateInvalidDay = Pattern.compile("^(---)\\d(\\d\\d)");
protected static Pattern dateInvalidMonth = Pattern.compile("(^\\d\\d\\d\\d-\\d\\d\\d-\\d\\d$|^\\d\\d\\d\\d-\\d\\d\\d.*|^\\d\\d\\d\\d-\\d\\d\\d-\\d\\dT.*)");
protected static Pattern dateInvalidYear = Pattern.compile("(^0(\\d\\d\\d\\d\\d*)-(\\d\\d)-(\\d\\d)|^0(\\d\\d\\d\\d)-(\\d\\d\\d)|^0(\\d\\d\\d\\d)-(\\d\\d)|^0(\\d\\d\\d\\d)|^0(\\d\\d\\d\\d\\d*)-(\\d\\d)-(\\d\\d)T(\\d\\d):(\\d\\d):(\\d\\d))");
public final static int YEAR = 0;
public final static int MONTH = 1;
public final static int DAY = 2;
public final static int HOUR = 3;
public final static int MINUTE = 4;
public final static int SECOND = 5;
public final static int MILLISECOND = 6;
/**
* Create a new date time value based on the given calendar. The calendar is
* <em>not</em> cloned, so it is the subclass's responsibility to make sure there are
* no external references to it that would allow for mutation.
*
* @param calendar the calendar to wrap into an XPath value
*/
protected AbstractDateTimeValue(XMLGregorianCalendar calendar) {
this.calendar = calendar;
}
protected AbstractDateTimeValue(String lexicalValue)
throws XPathException {
lexicalValue = StringValue.trimWhitespace(lexicalValue);
lexicalValue = normalizeDate(lexicalValue);
lexicalValue = normalizeTime(lexicalValue);
try {
this.calendar = TimeUtils.getInstance().newXMLGregorianCalendar(lexicalValue);
} catch (IllegalArgumentException e) {
throw new XPathException("err:FORG0001: illegal lexical form for date-time-like value '" + lexicalValue + "' " + e.getMessage(), e);
}
}
/**
* Return a calendar with the timezone field set, to be used for order comparison.
* If the original calendar did not specify a timezone, set the local timezone (unadjusted
* for daylight savings). The returned calendars will be totally ordered between themselves.
* We also set any missing fields to ensure that normalization doesn't discard important data!
* (This is probably a bug in the JAXP implementation, but the workaround doesn't hurt us,
* so it's faster to just fix it here.)
*
* @return the calendar represented by this object, with the timezone field filled in with an implicit value if necessary
*/
protected XMLGregorianCalendar getImplicitCalendar() {
if (implicitCalendar == null) {
implicitCalendar = (XMLGregorianCalendar) calendar.clone();
if (calendar.getTimezone() == DatatypeConstants.FIELD_UNDEFINED) {
implicitCalendar.setTimezone(TimeUtils.getInstance().getLocalTimezoneOffsetMinutes());
}
// fill in fields from default reference; don't have to worry about weird combinations of fields being set, since we control that on creation
switch(getType()) {
case Type.DATE: implicitCalendar.setTime(0,0,0); break;
case Type.TIME: implicitCalendar.setYear(1972); implicitCalendar.setMonth(12); implicitCalendar.setDay(31); break;
default:
}
implicitCalendar = implicitCalendar.normalize(); // the comparison routines will normalize it anyway, just do it once here
}
return implicitCalendar;
}
// TODO: method not currently used, apparently the XPath spec never needs to canonicalize
// date/times after all (see section 17.1.2 on casting)
protected XMLGregorianCalendar getCanonicalCalendar() {
if (canonicalCalendar == null) {
canonicalCalendar = getTrimmedCalendar().normalize();
}
return canonicalCalendar;
}
protected XMLGregorianCalendar getTrimmedCalendar() {
if (trimmedCalendar == null) {
trimmedCalendar = cloneXMLGregorianCalendar(calendar);
BigDecimal fract = trimmedCalendar.getFractionalSecond();
if (fract != null) {
// TODO: replace following algorithm in JDK 1.5 with fract.stripTrailingZeros();
String s = fract.toString();
int i = s.length();
while (i > 0 && s.charAt(i-1) == '0') i--;
if (i == 0) trimmedCalendar.setFractionalSecond(null);
else if (i != s.length()) trimmedCalendar.setFractionalSecond(new BigDecimal(s.substring(0, i)));
}
}
return trimmedCalendar;
}
protected abstract AbstractDateTimeValue createSameKind(XMLGregorianCalendar cal) throws XPathException;
protected long getTimeInMillis() {
// use getImplicitCalendar() rather than relying on toGregorianCalendar timezone defaulting
// to maintain consistency
return getImplicitCalendar().toGregorianCalendar().getTimeInMillis();
}
protected abstract QName getXMLSchemaType();
public String getStringValue() throws XPathException {
String r = getTrimmedCalendar().toXMLFormat();
// hacked to match the format mandated in XPath 2 17.1.2, which is different from the XML Schema canonical format
//if (r.charAt(r.length()-1) == 'Z') r = r.substring(0, r.length()-1) + "+00:00";
//Let's try these lexical transformations...
boolean startsWithDashDash = r.startsWith("--");
r = r.replaceAll("--", "");
if (startsWithDashDash)
r = "--" + r;
Matcher m = negativeDateStart.matcher(r);
if (m.matches()) {
int year = Integer.valueOf(m.group(1)).intValue();
DecimalFormat df = new DecimalFormat("0000");
r = "-" + df.format(year) + "-" + m.group(2);
}
return r;
}
public boolean effectiveBooleanValue() throws XPathException {
throw new XPathException("FORG0006: effective boolean value invalid operand type: " + Type.getTypeName(getType()));
}
public abstract AtomicValue convertTo(int requiredType) throws XPathException;
public int getPart(int part) {
switch (part) {
case YEAR: return calendar.getYear();
case MONTH: return calendar.getMonth();
case DAY: return calendar.getDay();
case HOUR: return calendar.getHour();
case MINUTE: return calendar.getMinute();
case SECOND: return calendar.getSecond();
case MILLISECOND: int mSec=calendar.getMillisecond();
if(mSec == DatatypeConstants.FIELD_UNDEFINED)
return 0;
else
return calendar.getMillisecond();
default: throw new IllegalArgumentException("Invalid argument to method getPart");
}
}
private static final Duration tzLowerBound = TimeUtils.getInstance().newDurationDayTime("-PT14H");
private static final Duration tzUpperBound = tzLowerBound.negate();
protected void validateTimezone(DayTimeDurationValue offset) throws XPathException {
Duration tz = offset.duration;
Number secs = tz.getField(DatatypeConstants.SECONDS);
if (secs != null && ((BigDecimal) secs).compareTo(BigDecimal.valueOf(0)) != 0)
throw new XPathException("duration " + offset + " has fractional minutes so cannot be used as a timezone offset");
if (! (
tz.equals(tzLowerBound) ||
tz.equals(tzUpperBound) ||
(tz.isLongerThan(tzLowerBound) && tz.isShorterThan(tzUpperBound))
))
throw new XPathException("duration " + offset + " outside valid timezone offset range");
}
public AbstractDateTimeValue adjustedToTimezone(DayTimeDurationValue offset) throws XPathException {
if (offset == null) offset = new DayTimeDurationValue(TimeUtils.getInstance().getLocalTimezoneOffsetMillis());
validateTimezone(offset);
XMLGregorianCalendar xgc = (XMLGregorianCalendar) calendar.clone();
if (xgc.getTimezone() != DatatypeConstants.FIELD_UNDEFINED) {
if (getType() == Type.DATE) xgc.setTime(0,0,0); // set the fields so we don't lose precision when shifting timezones
xgc = xgc.normalize();
xgc.add(offset.duration);
}
try {
xgc.setTimezone((int) (offset.getValue()/60));
} catch (IllegalArgumentException e) {
throw new XPathException("illegal timezone offset " + offset, e);
}
return createSameKind(xgc);
}
public AbstractDateTimeValue withoutTimezone() throws XPathException {
XMLGregorianCalendar xgc = (XMLGregorianCalendar) calendar.clone();
xgc.setTimezone(DatatypeConstants.FIELD_UNDEFINED);
return createSameKind(xgc);
}
public Sequence getTimezone() throws XPathException {
int tz = calendar.getTimezone();
if (tz == DatatypeConstants.FIELD_UNDEFINED) return Sequence.EMPTY_SEQUENCE;
return new DayTimeDurationValue(tz * 60000L);
}
public boolean compareTo(Collator collator, int operator, AtomicValue other) throws XPathException {
int cmp = compareTo(collator, other);
switch (operator) {
case Constants.EQ: return cmp == 0;
case Constants.NEQ: return cmp != 0;
case Constants.LT: return cmp < 0;
case Constants.LTEQ: return cmp <= 0;
case Constants.GT: return cmp > 0;
case Constants.GTEQ: return cmp >= 0;
default :
throw new XPathException("Unknown operator type in comparison");
}
}
public int compareTo(Collator collator, AtomicValue other) throws XPathException {
if (other.getType() == getType()) {
// filling in missing timezones with local timezone, should be total order as per XPath 2.0 10.4
int r = getImplicitCalendar().compare(((AbstractDateTimeValue) other).getImplicitCalendar());
if (r == DatatypeConstants.INDETERMINATE) throw new RuntimeException("indeterminate order between " + this + " and " + other);
return r;
}
throw new XPathException(
"Type error: cannot compare " + Type.getTypeName(getType()) + " to "
+ Type.getTypeName(other.getType()));
}
public AtomicValue max(Collator collator, AtomicValue other) throws XPathException {
AbstractDateTimeValue otherDate = other.getType() == getType() ? (AbstractDateTimeValue) other : (AbstractDateTimeValue) other.convertTo(getType());
return getImplicitCalendar().compare(otherDate.getImplicitCalendar()) > 0 ? this : other;
}
public AtomicValue min(Collator collator, AtomicValue other) throws XPathException {
AbstractDateTimeValue otherDate = other.getType() == getType() ? (AbstractDateTimeValue) other : (AbstractDateTimeValue) other.convertTo(getType());
return getImplicitCalendar().compare(otherDate.getImplicitCalendar()) < 0 ? this : other;
}
// override for xs:time
public ComputableValue plus(ComputableValue other) throws XPathException {
switch(other.getType()) {
case Type.YEAR_MONTH_DURATION:
case Type.DAY_TIME_DURATION:
return other.plus(this);
default:
throw new XPathException(
"Operand to plus should be of type xdt:dayTimeDuration or xdt:yearMonthDuration; got: "
+ Type.getTypeName(other.getType()));
}
}
public ComputableValue mult(ComputableValue other) throws XPathException {
throw new XPathException("multiplication is not supported for type " + Type.getTypeName(getType()));
}
public ComputableValue div(ComputableValue other) throws XPathException {
throw new XPathException("division is not supported for type " + Type.getTypeName(getType()));
}
public int conversionPreference(Class javaClass) {
if (javaClass.isAssignableFrom(DateValue.class)) return 0;
if (javaClass.isAssignableFrom(XMLGregorianCalendar.class)) return 1;
if (javaClass.isAssignableFrom(GregorianCalendar.class)) return 2;
if (javaClass == Date.class) return 3;
return Integer.MAX_VALUE;
}
public Object toJavaObject(Class target) throws XPathException {
if (target == Object.class || target.isAssignableFrom(DateValue.class))
return this;
else if (target.isAssignableFrom(XMLGregorianCalendar.class))
return calendar.clone();
else if (target.isAssignableFrom(GregorianCalendar.class))
return calendar.toGregorianCalendar();
else if (target == Date.class)
return calendar.toGregorianCalendar().getTime();
throw new XPathException("cannot convert value of type " + Type.getTypeName(getType()) + " to Java object of type " + target.getName());
}
/* (non-Javadoc)
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
public int compareTo(Object o)
{
final AtomicValue other = (AtomicValue)o;
if(Type.subTypeOf(other.getType(), Type.DATE_TIME))
try {
//TODO : find something that will consume less resources
return calendar.compare(TimeUtils.getInstance().newXMLGregorianCalendar(other.getStringValue()));
} catch (XPathException e) {
System.out.println("Failed to get string value of '" + other + "'");
//Why not ?
return Constants.SUPERIOR;
}
else
return getType() > other.getType() ? Constants.SUPERIOR : Constants.INFERIOR;
}
/**
* The method <code>normalizeDate</code>
*
* @param dateValue a <code>String</code> value
* @return a <code>String</code> value
* @exception XPathException if an error occurs
*/
public static String normalizeDate(String dateValue)
throws XPathException {
Matcher d = dateInvalidDay.matcher(dateValue);
Matcher m = dateInvalidMonth.matcher(dateValue);
Matcher y = dateInvalidYear.matcher(dateValue);
if (d.matches() || m.matches() || y.matches()) {
throw new XPathException("err:FORG0001: illegal lexical form for date-time-like value '" + dateValue + "'");
}
return dateValue;
}
/**
* The method <code>normalizeTime</code>
*
* @param timeValue a <code>String</code> value
* @return a <code>String</code> value
*/
public static String normalizeTime(String timeValue)
throws XPathException {
int hours = 0;
int mins = 0;
int secs = 0;
int mSecs = 0;
int tzHours = 0;
int tzMins = 0;
DecimalFormat df = new DecimalFormat("00");
DecimalFormat msf = new DecimalFormat("000");
Matcher m = timeNoTZ.matcher(timeValue);
if (m.matches()) {
hours = Integer.valueOf(m.group(1)).intValue();
mins = Integer.valueOf(m.group(2)).intValue();
secs = Integer.valueOf(m.group(3)).intValue();
if (mins >= 60 || mins < 0 || secs >= 60 || secs < 0) {
throw new XPathException("err:FORG0001: illegal lexical form for date-time-like value '" + timeValue + "'");
}
if (hours == 24) {
if (mins == 0) {
hours = 0;
} else {
throw new XPathException("err:FORG0001: illegal lexical form for date-time-like value '" + timeValue + "'. If hours is 24, minutes must be 00.");
}
}
// fixme!
timeValue = df.format(hours) + ":" + df.format(mins) + ":" + df.format(secs);
}
m = dateTimeNoTZ.matcher(timeValue);
if (m.matches()) {
String date = m.group(1);
DateValue dateValue = null;
hours = Integer.valueOf(m.group(3)).intValue();
mins = Integer.valueOf(m.group(4)).intValue();
secs = Integer.valueOf(m.group(5)).intValue();
if (mins >= 60 || mins < 0 || secs >= 60 || secs < 0) {
throw new XPathException("err:FORG0001: illegal lexical form for date-time-like value '" + timeValue + "'");
}
if (hours == 24) {
if (mins == 0) {
hours = 0;
dateValue = (DateValue) new DateValue(date).plus(new DayTimeDurationValue("P1D"));
} else {
throw new XPathException("err:FORG0001: illegal lexical form for date-time-like value '" + timeValue + "'. If hours is 24, minutes must be 00.");
}
}
// fixme!
timeValue = (dateValue == null ? date : dateValue.getStringValue()) + m.group(2) + df.format(hours) + ":" + df.format(mins) + ":" + df.format(secs);
}
m = dateTimeMsWTZ.matcher(timeValue);
if (m.matches()) {
hours = Integer.valueOf(m.group(2)).intValue();
mins = Integer.valueOf(m.group(3)).intValue();
secs = Integer.valueOf(m.group(4)).intValue();
mSecs = Integer.valueOf(m.group(6)).intValue();
tzHours = Integer.valueOf(m.group(8)).intValue();
tzMins = Integer.valueOf(m.group(9)).intValue();
if (mins >= 60 || mins < 0 || tzMins >= 60 || tzMins < 0) {
throw new XPathException("err:FORG0001: illegal lexical form for date-time-like value '" + timeValue + "'");
}
if (hours == 24) {
if (mins == 0) {
hours = 0;
} else {
throw new XPathException("err:FORG0001: illegal lexical form for date-time-like value '" + timeValue + "'. If hours is 24, minutes must be 00.");
}
}
// fixme!
timeValue = m.group(1) + df.format(hours) + ":" + df.format(mins) + ":" + df.format(secs) + m.group(5) + msf.format(mSecs) + m.group(7) + df.format(tzHours) + ":" + df.format(tzMins);
}
m = timeMsWTZ.matcher(timeValue);
if (m.matches()) {
hours = Integer.valueOf(m.group(1)).intValue();
mins = Integer.valueOf(m.group(2)).intValue();
secs = Integer.valueOf(m.group(3)).intValue();
mSecs = Integer.valueOf(m.group(5)).intValue();
tzHours = Integer.valueOf(m.group(7)).intValue();
tzMins = Integer.valueOf(m.group(8)).intValue();
if (mins >= 60 || mins < 0 || tzMins >= 60 || tzMins < 0) {
throw new XPathException("err:FORG0001: illegal lexical form for date-time-like value '" + timeValue + "'");
}
if (hours == 24) {
if (mins == 0) {
hours = 0;
} else {
throw new XPathException("err:FORG0001: illegal lexical form for date-time-like value '" + timeValue + "'. If hours is 24, minutes must be 00.");
}
}
timeValue = df.format(hours) + ":" + df.format(mins) + ":" + df.format(secs) + m.group(4) + msf.format(mSecs) + m.group(6) + df.format(tzHours) + ":" + df.format(tzMins);
}
m = gYearWUTCTZ.matcher(timeValue);
if (m.matches()) {
tzHours = Integer.valueOf(m.group(3)).intValue();
tzMins = Integer.valueOf(m.group(4)).intValue();
if (tzMins >= 60 || tzMins < 0) {
throw new XPathException("err:FORG0001: illegal lexical form for date-time-like value '" + timeValue + "'");
}
if (tzHours == 24) {
if (tzMins == 0) {
tzHours = 0;
} else {
throw new XPathException("err:FORG0001: illegal lexical form for date-time-like value '" + timeValue + "'. If hours is 24, minutes must be 00.");
}
}
// fixme!
timeValue = m.group(1) + m.group(2) + df.format(tzHours) + ":" + df.format(tzMins);
}
m = gDayWTZ.matcher(timeValue);
if (m.matches()) {
tzHours = Integer.valueOf(m.group(3)).intValue();
tzMins = Integer.valueOf(m.group(4)).intValue();
if (tzMins >= 60 || tzMins < 0) {
throw new XPathException("err:FORG0001: illegal lexical form for date-time-like value '" + timeValue + "'");
}
if (tzHours == 24) {
if (tzMins == 0) {
tzHours = 0;
} else {
throw new XPathException("err:FORG0001: illegal lexical form for date-time-like value '" + timeValue + "'. If hours is 24, minutes must be 00.");
}
}
// fixme!
timeValue = m.group(1) + m.group(2) + df.format(tzHours) + ":" + df.format(tzMins);
}
m = gMonthWTZ.matcher(timeValue);
if (m.matches()) {
tzHours = Integer.valueOf(m.group(3)).intValue();
tzMins = Integer.valueOf(m.group(4)).intValue();
if (tzMins >= 60 || tzMins < 0) {
throw new XPathException("err:FORG0001: illegal lexical form for date-time-like value '" + timeValue + "'");
}
if (tzHours == 24) {
if (tzMins == 0) {
tzHours = 0;
} else {
throw new XPathException("err:FORG0001: illegal lexical form for date-time-like value '" + timeValue + "'. If hours is 24, minutes must be 00.");
}
}
// fixme!
timeValue = m.group(1) + m.group(2) + df.format(tzHours) + ":" + df.format(tzMins);
}
m = gYearMonthWTZ.matcher(timeValue);
if (m.matches()) {
tzHours = Integer.valueOf(m.group(3)).intValue();
tzMins = Integer.valueOf(m.group(4)).intValue();
if (tzMins >= 60 || tzMins < 0) {
throw new XPathException("err:FORG0001: illegal lexical form for date-time-like value '" + timeValue + "'");
}
if (tzHours == 24) {
if (tzMins == 0) {
tzHours = 0;
} else {
throw new XPathException("err:FORG0001: illegal lexical form for date-time-like value '" + timeValue + "'. If hours is 24, minutes must be 00.");
}
}
// fixme!
timeValue = m.group(1) + m.group(2) + df.format(tzHours) + ":" + df.format(tzMins);
}
m = gMonthDayWTZ.matcher(timeValue);
if (m.matches()) {
tzHours = Integer.valueOf(m.group(3)).intValue();
tzMins = Integer.valueOf(m.group(4)).intValue();
if (tzMins >= 60 || tzMins < 0) {
throw new XPathException("err:FORG0001: illegal lexical form for date-time-like value '" + timeValue + "'");
}
if (tzHours == 24) {
if (tzMins == 0) {
tzHours = 0;
} else {
throw new XPathException("err:FORG0001: illegal lexical form for date-time-like value '" + timeValue + "'. If hours is 24, minutes must be 00.");
}
}
// fixme!
timeValue = m.group(1) + m.group(2) + df.format(tzHours) + ":" + df.format(tzMins);
}
m = dateWTZ.matcher(timeValue);
if (m.matches()) {
hours = Integer.valueOf(m.group(3)).intValue();
mins = Integer.valueOf(m.group(4)).intValue();
if (mins >= 60 || mins < 0 || tzMins >= 60 || tzMins < 0) {
throw new XPathException("err:FORG0001: illegal lexical form for date-time-like value '" + timeValue + "'");
}
if (hours == 24) {
if (mins == 0) {
hours = 0;
} else {
throw new XPathException("err:FORG0001: illegal lexical form for date-time-like value '" + timeValue + "'. If hours is 24, minutes must be 00.");
}
}
// fixme!
timeValue = m.group(1) + m.group(2) + df.format(hours) + ":" + df.format(mins);
}
return timeValue;
}
/**
* Utility method that is able to clone a calendar whose year is 0
* (whatever a year 0 means).
* It looks like the JDK is unable to do that.
* @param calendar The Calendar to clone
* @return the cloned Calendar
*/
public static XMLGregorianCalendar cloneXMLGregorianCalendar(XMLGregorianCalendar calendar) {
boolean hacked = false;
if (calendar.getYear() == 0) {
calendar.setYear(1);
hacked = true;
}
XMLGregorianCalendar result = (XMLGregorianCalendar)calendar.clone();
if (hacked) {
//reset everything
calendar.setYear(0);
//-1 could also be considered
result.setYear(0);
}
return result;
}
}