1 package net.sf.saxon.value; 2 import net.sf.saxon.Controller; 3 import net.sf.saxon.ConversionContext; 4 import net.sf.saxon.expr.XPathContext; 5 import net.sf.saxon.functions.Component; 6 import net.sf.saxon.om.FastStringBuffer; 7 import net.sf.saxon.trans.DynamicError; 8 import net.sf.saxon.trans.XPathException; 9 import net.sf.saxon.type.BuiltInAtomicType; 10 import net.sf.saxon.type.ItemType; 11 import net.sf.saxon.type.Type; 12 import net.sf.saxon.type.ValidationException; 13 14 import java.math.BigDecimal ; 15 import java.util.*; 16 17 20 21 public final class DateTimeValue extends CalendarValue { 22 23 private Date UTCDate = null; 27 28 33 34 public static DateTimeValue getCurrentDateTime(XPathContext context) { 35 Controller c; 36 if (context==null || (c = context.getController()) == null) { 37 return new DateTimeValue(new GregorianCalendar(), true); 40 } else { 41 return c.getCurrentDateTime(); 42 } 43 } 44 45 50 51 public DateTimeValue(GregorianCalendar calendar, boolean tzSpecified) { 52 this.calendar = calendar; 53 zoneSpecified = tzSpecified; 54 } 55 56 62 63 public DateTimeValue(DateValue date, TimeValue time) throws XPathException { 64 SecondsDurationValue tz1 = (SecondsDurationValue)date.getComponent(Component.TIMEZONE); 65 SecondsDurationValue tz2 = (SecondsDurationValue)time.getComponent(Component.TIMEZONE); 66 zoneSpecified = (tz1 != null || tz2 != null); 67 if (tz1 != null && tz2 != null && !tz1.equals(tz2)) { 68 DynamicError err = new DynamicError("Supplied date and time are in different timezones"); 69 err.setErrorCode("FORG0008"); 70 throw err; 71 } 72 TimeZone zone; 75 SecondsDurationValue tz = (tz1 == null ? tz2 : tz1); 76 int zoneOffset = (tz == null ? 0 : (int)tz.getLengthInMilliseconds()); 77 zone = (tz == null ? TimeZone.getTimeZone("GMT") : new SimpleTimeZone(zoneOffset, "LLL")); 78 calendar = new GregorianCalendar(zone); 79 calendar.setLenient(false); 80 final int year = (int)((IntegerValue)date.getComponent(Component.YEAR)).longValue(); 81 final int month = (int)((IntegerValue)date.getComponent(Component.MONTH)).longValue(); 82 final int day = (int)((IntegerValue)date.getComponent(Component.DAY)).longValue(); 83 final int hour = (int)((IntegerValue)time.getComponent(Component.HOURS)).longValue(); 84 final int minute = (int)((IntegerValue)time.getComponent(Component.MINUTES)).longValue(); 85 final BigDecimal secs = ((DecimalValue)time.getComponent(Component.SECONDS)).getValue(); 86 final int second = secs.intValue(); 87 final int millisec = secs.multiply(BigDecimal.valueOf(1000)).intValue() % 1000; 88 89 calendar.set(Math.abs(year), month-1, day, hour, minute, second); 90 if (year < 0) { 91 calendar.set(Calendar.ERA, GregorianCalendar.BC); 92 } 93 calendar.set(Calendar.MILLISECOND, millisec); 94 calendar.set(Calendar.ZONE_OFFSET, zoneOffset); 95 calendar.set(Calendar.DST_OFFSET, 0); 96 } 97 101 102 public DateTimeValue(CharSequence s) throws XPathException { 103 zoneSpecified = false; 105 StringTokenizer tok = new StringTokenizer(trimWhitespace(s).toString(), "-:.+TZ", true); 106 try { 107 if (!tok.hasMoreElements()) badDate("too short"); 108 String part = (String )tok.nextElement(); 109 int era = +1; 110 if ("+".equals(part)) { 111 part = (String )tok.nextElement(); 112 } else if ("-".equals(part)) { 113 era = -1; 114 part = (String )tok.nextElement(); 115 } 116 int year = Integer.parseInt(part) * era; 117 if (part.length() < 4) badDate("Year is less than four digits"); 118 if (year==0) badDate("Year zero is not allowed"); 119 if (!tok.hasMoreElements()) badDate("Too short"); 120 if (!"-".equals(tok.nextElement())) badDate("Wrong delimiter after year"); 121 122 if (!tok.hasMoreElements()) badDate("Too short"); 123 part = (String )tok.nextElement(); 124 int month = Integer.parseInt(part); 125 if (part.length() != 2) badDate("Month must be two digits"); 126 if (month < 1 || month > 12) badDate("Month is out of range"); 127 if (!tok.hasMoreElements()) badDate("Too short"); 128 if (!"-".equals(tok.nextElement())) badDate("Wrong delimiter after month"); 129 130 if (!tok.hasMoreElements()) badDate("Too short"); 131 part = (String )tok.nextElement(); 132 int day = Integer.parseInt(part); 133 if (part.length() != 2) badDate("Day must be two digits"); 134 if (day < 1 || day > 31) badDate("Day is out of range"); 135 if (!tok.hasMoreElements()) badDate("Too short"); 136 if (!"T".equals(tok.nextElement())) badDate("Wrong delimiter after day"); 137 138 if (!tok.hasMoreElements()) badDate("Too short"); 139 part = (String )tok.nextElement(); 140 int hour = Integer.parseInt(part); 141 if (part.length() != 2) badDate("Hour must be two digits"); 142 if (hour > 24) badDate("Hour is out of range"); 143 if (!tok.hasMoreElements()) badDate("Too short"); 144 if (!":".equals(tok.nextElement())) badDate("Wrong delimiter after hour"); 145 146 if (!tok.hasMoreElements()) badDate("Too short"); 147 part = (String )tok.nextElement(); 148 int minute = Integer.parseInt(part); 149 if (part.length() != 2) badDate("Minute must be two digits"); 150 if (minute > 59) badDate("Minute is out of range"); 151 if (hour == 24 && minute != 0) badDate("If hour is 24, minute must be 00"); 152 if (!tok.hasMoreElements()) badDate("Too short"); 153 if (!":".equals(tok.nextElement())) badDate("Wrong delimiter after minute"); 154 155 if (!tok.hasMoreElements()) badDate("Too short"); 156 part = (String )tok.nextElement(); 157 int second = Integer.parseInt(part); 158 if (part.length() != 2) badDate("Second must be two digits"); 159 if (second > 61) badDate("Second is out of range"); 160 if (hour == 24 && second != 0) badDate("If hour is 24, second must be 00"); 161 162 int millisecond = 0; 163 int tz = 0; 164 165 int state = 0; 166 while (tok.hasMoreElements()) { 167 if (state==9) { 168 badDate("Characters after the end"); 169 } 170 String delim = (String )tok.nextElement(); 171 if (".".equals(delim)) { 172 if (state != 0) { 173 badDate("Decimal separator occurs twice"); 174 } 175 part = (String )tok.nextElement(); 176 double fractionalSeconds = Double.parseDouble('.' + part); 177 millisecond = (int)(Math.round(fractionalSeconds * 1000)); 178 if (hour == 24 && millisecond != 0) { 179 badDate("If hour is 24, milliseconds must be 0"); 180 } 181 state = 1; 182 } else if ("Z".equals(delim)) { 183 if (state > 1) { 184 badDate("Z cannot occur here"); 185 } 186 zoneSpecified = true; 187 tz = 0; 188 state = 9; } else if ("+".equals(delim) || "-".equals(delim)) { 190 if (state > 1) { 191 badDate(delim + " cannot occur here"); 192 } 193 state = 2; 194 zoneSpecified = true; 195 if (!tok.hasMoreElements()) badDate("Missing timezone"); 196 part = (String )tok.nextElement(); 197 if (part.length() != 2) badDate("Timezone hour must be two digits"); 198 tz = Integer.parseInt(part) * 60; 199 if (tz > 14*60) badDate("Timezone hour is out of range"); 200 if (tz > 12*60) badDate("Because of Java limitations, Saxon currently limits the timezone to +/- 12 hours"); 201 if ("-".equals(delim)) tz = -tz; 202 203 } else if (":".equals(delim)) { 204 if (state != 2) { 205 badDate("Misplaced ':'"); 206 } 207 state = 9; 208 part = (String )tok.nextElement(); 209 int tzminute = Integer.parseInt(part); 210 if (part.length() != 2) badDate("Timezone minute must be two digits"); 211 if (tzminute > 59) badDate("Timezone minute is out of range"); 212 if (tz<0) tzminute = -tzminute; 213 tz += tzminute; 214 } else { 215 badDate("Timezone format is incorrect"); 216 } 217 } 218 219 if (state == 2 || state == 3) { 220 badDate("Timezone incomplete"); 221 } 222 223 boolean adjust = false; 224 if (hour == 24) { 225 hour = 0; 226 adjust = true; 227 } 228 TimeZone zone = new SimpleTimeZone(tz*60000, "LLL"); 231 calendar = new GregorianCalendar(zone); 232 calendar.setLenient(false); 233 calendar.set(Math.abs(year), month-1, day, hour, minute, second); 234 if (year < 0) { 235 calendar.set(Calendar.ERA, GregorianCalendar.BC); 236 } 237 calendar.set(Calendar.MILLISECOND, millisecond); 238 calendar.set(Calendar.ZONE_OFFSET, tz*60000); 239 calendar.set(Calendar.DST_OFFSET, 0); 240 241 try { 242 calendar.getTime(); 243 } catch (IllegalArgumentException err) { 244 badDate("Non-existent date"); 245 } 246 247 if (adjust) { 248 calendar.add(Calendar.DAY_OF_MONTH, 1); 249 } 250 251 252 } catch (NumberFormatException err) { 253 badDate("Non-numeric component"); 254 } 255 } 256 257 private void badDate(String msg) throws XPathException { 258 DynamicError err = new DynamicError("Invalid dateTime value. " + msg); 259 err.setErrorCode("FORG0001"); 260 throw err; 261 } 262 263 264 269 270 public Date getUTCDate() { 271 if (UTCDate==null) { 273 UTCDate = calendar.getTime(); 274 } 275 return UTCDate; 276 } 277 278 281 282 public Calendar getCalendar() { 283 return calendar; 284 } 285 286 292 293 public AtomicValue convertPrimitive(BuiltInAtomicType requiredType, boolean validate, ConversionContext conversion) { 294 try { 295 switch(requiredType.getPrimitiveType()) { 296 case Type.DATE_TIME: 297 case Type.ATOMIC: 298 case Type.ITEM: 299 return this; 300 case Type.DATE: 301 String ds = getStringValue(); 302 int sep = ds.indexOf('T'); 303 if (zoneSpecified) { 304 int z = ds.indexOf('Z', sep); 305 if (z < 0) { 306 z = ds.indexOf('+', sep); 307 } 308 if (z < 0) { 309 z = ds.indexOf('-', sep); 310 } 311 if (z < 0) { 312 throw new IllegalArgumentException ("Internal date formatting error " + ds); 314 } 315 return new DateValue(ds.substring(0, sep) + ds.substring(z)); 316 } else { 317 return new DateValue(ds.substring(0, sep)); 318 } 319 case Type.TIME: 320 ds = getStringValue(); 321 sep = ds.indexOf('T'); 322 return new TimeValue(ds.substring(sep+1)); 323 324 case Type.G_YEAR: 325 return(convertPrimitive(Type.DATE_TYPE, validate, conversion) 326 .convertPrimitive(Type.G_YEAR_TYPE, validate, conversion)); 327 328 case Type.G_YEAR_MONTH: 329 return(convertPrimitive(Type.DATE_TYPE, validate, conversion) 330 .convertPrimitive(Type.G_YEAR_MONTH_TYPE, validate, conversion)); 331 332 case Type.G_MONTH: 333 return(convertPrimitive(Type.DATE_TYPE, validate, conversion) 334 .convertPrimitive(Type.G_MONTH_TYPE, validate, conversion)); 335 336 case Type.G_MONTH_DAY: 337 return(convertPrimitive(Type.DATE_TYPE, validate, conversion) 338 .convertPrimitive(Type.G_MONTH_DAY_TYPE, validate, conversion)); 339 340 case Type.G_DAY: 341 return(convertPrimitive(Type.DATE_TYPE, validate, conversion) 342 .convertPrimitive(Type.G_DAY_TYPE, validate, conversion)); 343 344 case Type.STRING: 345 return new StringValue(getStringValueCS()); 346 347 case Type.UNTYPED_ATOMIC: 348 return new UntypedAtomicValue(getStringValueCS()); 349 350 default: 351 ValidationException err = new ValidationException("Cannot convert dateTime to " + 352 requiredType.getDisplayName()); 353 err.setErrorCode("FORG0001"); 355 return new ValidationErrorValue(err); 356 } 357 } catch (ValidationException e) { 358 return new ValidationErrorValue(e); 359 } catch (XPathException e) { 360 return new ValidationErrorValue(new ValidationException(e)); 361 } 362 } 363 364 369 370 public String getStringValue() { 371 372 FastStringBuffer sb = new FastStringBuffer(30); 373 int era = calendar.get(GregorianCalendar.ERA); 374 int year = calendar.get(Calendar.YEAR); 375 if (era == GregorianCalendar.BC) { 376 sb.append('-'); 377 } 378 if (year<0) { 379 sb.append('-'); 380 year = -year; 381 } 382 appendString(sb, year, (year>9999 ? (calendar.get(Calendar.YEAR)+"").length() : 4)); 383 sb.append('-'); 384 appendString(sb, calendar.get(Calendar.MONTH)+1, 2); 385 sb.append('-'); 386 appendString(sb, calendar.get(Calendar.DATE), 2); 387 sb.append('T'); 388 appendString(sb, calendar.get(Calendar.HOUR_OF_DAY), 2); 389 sb.append(':'); 390 appendString(sb, calendar.get(Calendar.MINUTE), 2); 391 sb.append(':'); 392 appendSeconds(calendar, sb); 393 394 if (zoneSpecified) { 395 appendTimezone(calendar, sb); 396 } 397 398 return sb.toString(); 399 400 } 401 402 static void appendString(FastStringBuffer sb, int value, int size) { 403 String s = "000000"+value; 404 sb.append( s.substring(s.length()-size) ); 405 } 406 407 static void appendSeconds(Calendar calendar, FastStringBuffer sb) { 408 appendString(sb, calendar.get(Calendar.SECOND), 2); 409 int millis = calendar.get(Calendar.MILLISECOND); 410 if (millis != 0) { 411 sb.append('.'); 412 String m = calendar.get(Calendar.MILLISECOND) + ""; 413 while (m.length() < 3) m = '0' + m; 414 while (m.endsWith("0")) m = m.substring(0, m.length()-1); 415 sb.append(m); 416 } 417 return; 418 } 419 420 428 429 public static void appendTimezone(Calendar calendar, FastStringBuffer sb) { 430 int timeZoneOffset = (calendar.get(Calendar.ZONE_OFFSET) + 431 calendar.get(Calendar.DST_OFFSET)) / 60000; 432 appendTimezone(timeZoneOffset, sb); 433 } 434 435 443 444 static void appendTimezone(int timeZoneOffset, FastStringBuffer sb) { 445 if (timeZoneOffset == 0) { 446 sb.append('Z'); 447 } else { 448 sb.append((timeZoneOffset<0 ? "-" : "+")); 449 int tzo = timeZoneOffset; 450 if (tzo < 0) tzo = -tzo; 451 int tzhours = tzo / 60; 452 appendString(sb, tzhours, 2); 453 sb.append(':'); 454 int tzminutes = tzo % 60; 455 appendString(sb, tzminutes, 2); 456 } 457 return; 458 } 459 460 464 465 public ItemType getItemType() { 466 return Type.DATE_TIME_TYPE; 467 } 468 469 474 475 public CalendarValue removeTimezone() { 476 return new DateTimeValue(calendar, false); 477 } 478 479 485 486 public CalendarValue setTimezone(SecondsDurationValue tz) throws XPathException { 487 if (zoneSpecified) { 488 SimpleTimeZone stz = new SimpleTimeZone((int)(tz.getLengthInSeconds()*1000), "xxz"); 489 GregorianCalendar cal = new GregorianCalendar(stz); 490 cal.setTime(getUTCDate()); 491 return new DateTimeValue(cal, true); 492 } else { 493 FastStringBuffer sb = new FastStringBuffer(10); 494 sb.append(getStringValue()); 495 appendTimezone((int)(tz.getLengthInSeconds()/60.0), sb); 496 return new DateTimeValue(sb); 497 } 498 } 499 500 507 508 public CalendarValue add(DurationValue duration) throws XPathException { 509 if (duration instanceof SecondsDurationValue) { 510 double seconds = duration.getLengthInSeconds(); 511 GregorianCalendar cal2 = (GregorianCalendar)calendar.clone(); 512 cal2.add(Calendar.SECOND, (int)seconds); 513 cal2.add(Calendar.MILLISECOND, (int)((seconds % 1) * 1000)); 514 return new DateTimeValue(cal2, zoneSpecified); 515 } else if (duration instanceof MonthDurationValue) { 516 int months = ((MonthDurationValue)duration).getLengthInMonths(); 517 GregorianCalendar cal2 = (GregorianCalendar)calendar.clone(); 518 cal2.add(Calendar.MONTH, months); 519 return new DateTimeValue(cal2, zoneSpecified); 520 } else { 521 DynamicError err = new DynamicError( 522 "DateTime arithmetic is not supported on xs:duration, only on its subtypes"); 523 err.setIsTypeError(true); 524 throw err; 525 } 526 } 527 528 535 536 public SecondsDurationValue subtract(CalendarValue other, ConversionContext context) throws XPathException { 537 if (!(other instanceof DateTimeValue)) { 538 DynamicError err = new DynamicError( 539 "First operand of '-' is a dateTime, but the second is not"); 540 err.setIsTypeError(true); 541 throw err; 542 } 543 return super.subtract(other, context); 544 } 545 546 549 550 public Object convertToJava(Class target, XPathContext context) throws XPathException { 551 if (target.isAssignableFrom(Date.class)) { 552 return getUTCDate(); 553 } else if (target.isAssignableFrom(DateTimeValue.class)) { 554 return this; 555 } else if (target==String .class || target==CharSequence .class) { 556 return getStringValue(); 557 } else if (target==Object .class) { 558 return getStringValue(); 559 } else { 560 Object o = super.convertToJava(target, context); 561 if (o == null) { 562 throw new DynamicError("Conversion of dateTime to " + target.getName() + 563 " is not supported"); 564 } 565 return o; 566 } 567 } 568 569 573 574 public AtomicValue getComponent(int component) throws XPathException { 575 switch (component) { 576 case Component.YEAR: 577 return new IntegerValue(calendar.get(Calendar.YEAR)); 578 case Component.MONTH: 579 return new IntegerValue(calendar.get(Calendar.MONTH) + 1); 580 case Component.DAY: 581 return new IntegerValue(calendar.get(Calendar.DATE)); 582 case Component.HOURS: 583 return new IntegerValue(calendar.get(Calendar.HOUR_OF_DAY)); 584 case Component.MINUTES: 585 return new IntegerValue(calendar.get(Calendar.MINUTE)); 586 case Component.SECONDS: 587 FastStringBuffer sb = new FastStringBuffer(10); 588 appendSeconds(calendar, sb); 589 return DecimalValue.makeDecimalValue(sb, false); 590 case Component.TIMEZONE: 591 if (zoneSpecified) { 592 int tzmsecs = (calendar.get(Calendar.ZONE_OFFSET) + 593 calendar.get(Calendar.DST_OFFSET)); 594 return SecondsDurationValue.fromMilliseconds(tzmsecs); 595 } else { 596 return null; 597 } 598 default: 599 throw new IllegalArgumentException ("Unknown component for dateTime: " + component); 600 } 601 } 602 603 604 618 619 public int compareTo(Object other) { 620 if (!(other instanceof DateTimeValue)) { 622 throw new ClassCastException ("DateTime values are not comparable to " + other.getClass()); 623 } 624 DateTimeValue v1 = this; 625 DateTimeValue v2 = (DateTimeValue)other; 626 if (v1.zoneSpecified == v2.zoneSpecified) { 627 return getUTCDate().compareTo(((DateTimeValue)other).getUTCDate()); 629 } else { 630 try { 632 GregorianCalendar cal = new GregorianCalendar(); 633 int tz = (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / 1000; 634 SecondsDurationValue tzd = SecondsDurationValue.fromSeconds(tz); 635 if (!v1.zoneSpecified) { 636 v1 = (DateTimeValue)v1.setTimezone(tzd); 637 } 638 if (!v2.zoneSpecified) { 639 v2 = (DateTimeValue)v2.setTimezone(tzd); 640 } 641 return v1.getUTCDate().compareTo(v2.getUTCDate()); 642 } catch (XPathException e) { 643 throw new AssertionError ("Java timezone is out of range"); 644 } 645 } 646 } 647 648 659 660 public int compareTo(CalendarValue other, ConversionContext conversion) { 661 if (!(other instanceof DateTimeValue)) { 662 throw new ClassCastException ("DateTime values are not comparable to " + other.getClass()); 663 } 664 DateTimeValue v1 = this; 665 DateTimeValue v2 = (DateTimeValue)other; 666 if (v1.zoneSpecified == v2.zoneSpecified) { 667 return getUTCDate().compareTo(((DateTimeValue)other).getUTCDate()); 669 } else { 670 try { 672 int tz = conversion.getImplicitTimezone() * 60000; 673 SecondsDurationValue tzd = SecondsDurationValue.fromMilliseconds(tz); 674 if (!v1.zoneSpecified) { 675 v1 = (DateTimeValue)v1.setTimezone(tzd); 676 } 677 if (!v2.zoneSpecified) { 678 v2 = (DateTimeValue)v2.setTimezone(tzd); 679 } 680 return v1.getUTCDate().compareTo(v2.getUTCDate()); 681 } catch (XPathException e) { 682 throw new AssertionError ("Java timezone is out of range"); 683 } 684 } 685 } 686 687 688 689 public boolean equals(Object other) { 690 return compareTo(other) == 0; 691 } 692 693 public int hashCode() { 694 if (zoneSpecified) { 695 return getUTCDate().hashCode(); 696 } else { 697 try { 698 GregorianCalendar cal = new GregorianCalendar(); 699 int tz = (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / 60000; 700 SecondsDurationValue tzd = SecondsDurationValue.fromSeconds(tz); 701 DateTimeValue v1 = (DateTimeValue)setTimezone(tzd); 702 return v1.getUTCDate().hashCode(); 703 } catch (XPathException e) { 704 throw new AssertionError ("Java timezone is out of range"); 705 } 706 } 707 } 708 709 } 710 711 729 | Popular Tags |