001: package net.sf.saxon.value;
002:
003: import net.sf.saxon.Err;
004: import net.sf.saxon.expr.XPathContext;
005: import net.sf.saxon.functions.Component;
006: import net.sf.saxon.om.FastStringBuffer;
007: import net.sf.saxon.trans.DynamicError;
008: import net.sf.saxon.trans.IndependentContext;
009: import net.sf.saxon.trans.XPathException;
010: import net.sf.saxon.type.*;
011:
012: import java.util.*;
013:
014: /**
015: * A value of type Date. Note that a Date may include a TimeZone.
016: */
017:
018: public class DateValue extends CalendarValue {
019:
020: protected int year; // unlike the lexical representation, includes a year zero
021: protected byte month;
022: protected byte day;
023:
024: /**
025: * Default constructor needed for subtyping
026: */
027:
028: protected DateValue() {
029: }
030:
031: /**
032: * Constructor given a year, month, and day. Performs no validation.
033: */
034:
035: protected DateValue(int year, byte month, byte day) {
036: this .year = year;
037: this .month = month;
038: this .day = day;
039: }
040:
041: /**
042: * Constructor given a year, month, and day, and timezone. Performs no validation.
043: */
044:
045: protected DateValue(int year, byte month, byte day, int tz) {
046: this .year = year;
047: this .month = month;
048: this .day = day;
049: setTimezoneInMinutes(tz);
050: }
051:
052: /**
053: * Constructor: create a dateTime value from a supplied string, in
054: * ISO 8601 format
055: */
056: public DateValue(CharSequence s) throws XPathException {
057: setLexicalValue(s);
058: }
059:
060: /**
061: * Create a DateValue
062: * @param calendar the absolute date/time value
063: * @param tz the timezone offset from UTC in minutes
064: */
065: public DateValue(GregorianCalendar calendar, int tz) {
066: int era = calendar.get(GregorianCalendar.ERA);
067: year = calendar.get(Calendar.YEAR);
068: if (era == GregorianCalendar.BC) {
069: year = -year;
070: }
071: month = (byte) (calendar.get(Calendar.MONTH) + 1);
072: day = (byte) (calendar.get(Calendar.DATE));
073: setTimezoneInMinutes(tz);
074: }
075:
076: /**
077: * Initialize the DateValue using a character string in the format yyyy-mm-dd and an optional time zone.
078: * Input must have format [+|-]yyyy-mm-dd[([+|-]hh:mm | Z)]
079: * @param s the supplied string value
080: * @throws net.sf.saxon.trans.XPathException
081: */
082: public void setLexicalValue(CharSequence s) throws XPathException {
083: StringTokenizer tok = new StringTokenizer(trimWhitespace(s)
084: .toString(), "-:+Z", true);
085: try {
086: if (!tok.hasMoreElements())
087: badDate("Too short", s);
088: String part = (String) tok.nextElement();
089: int era = +1;
090: if ("+".equals(part)) {
091: part = (String) tok.nextElement();
092: } else if ("-".equals(part)) {
093: era = -1;
094: part = (String) tok.nextElement();
095: }
096:
097: if (part.length() < 4)
098: badDate("Year is less than four digits", s);
099: year = Integer.parseInt(part) * era;
100: if (year == 0)
101: badDate("Year zero is not allowed", s);
102: if (era < 0) {
103: year++; // internal representation allows a year zero.
104: }
105: if (!tok.hasMoreElements())
106: badDate("Too short", s);
107: if (!"-".equals(tok.nextElement()))
108: badDate("Wrong delimiter after year", s);
109:
110: if (!tok.hasMoreElements())
111: badDate("Too short", s);
112: part = (String) tok.nextElement();
113: if (part.length() != 2)
114: badDate("Month must be two digits", s);
115: month = (byte) Integer.parseInt(part);
116: if (month < 1 || month > 12)
117: badDate("Month is out of range", s);
118: if (!tok.hasMoreElements())
119: badDate("Too short", s);
120: if (!"-".equals(tok.nextElement()))
121: badDate("Wrong delimiter after month", s);
122:
123: if (!tok.hasMoreElements())
124: badDate("Too short", s);
125: part = (String) tok.nextElement();
126: if (part.length() != 2)
127: badDate("Day must be two digits", s);
128: day = (byte) Integer.parseInt(part);
129: if (day < 1 || day > 31)
130: badDate("Day is out of range", s);
131:
132: int tzOffset;
133: if (tok.hasMoreElements()) {
134:
135: String delim = (String) tok.nextElement();
136:
137: if ("Z".equals(delim)) {
138: tzOffset = 0;
139: if (tok.hasMoreElements())
140: badDate("Continues after 'Z'", s);
141: setTimezoneInMinutes(tzOffset);
142:
143: } else if (!(!"+".equals(delim) && !"-".equals(delim))) {
144: if (!tok.hasMoreElements())
145: badDate("Missing timezone", s);
146: part = (String) tok.nextElement();
147: int tzhour = Integer.parseInt(part);
148: if (part.length() != 2)
149: badDate("Timezone hour must be two digits", s);
150: if (tzhour > 14)
151: badDate("Timezone hour is out of range", s);
152: //if (tzhour > 12) badDate("Because of Java limitations, Saxon currently limits the timezone to +/- 12 hours", s);
153: if (!tok.hasMoreElements())
154: badDate("No minutes in timezone", s);
155: if (!":".equals(tok.nextElement()))
156: badDate("Wrong delimiter after timezone hour",
157: s);
158:
159: if (!tok.hasMoreElements())
160: badDate("No minutes in timezone", s);
161: part = (String) tok.nextElement();
162: int tzminute = Integer.parseInt(part);
163: if (part.length() != 2)
164: badDate("Timezone minute must be two digits", s);
165: if (tzminute > 59)
166: badDate("Timezone minute is out of range", s);
167: if (tok.hasMoreElements())
168: badDate("Continues after timezone", s);
169:
170: tzOffset = (tzhour * 60 + tzminute);
171: if ("-".equals(delim))
172: tzOffset = -tzOffset;
173: setTimezoneInMinutes(tzOffset);
174:
175: } else {
176: badDate("Timezone format is incorrect", s);
177: }
178: }
179:
180: if (!isValidDate(year, month, day)) {
181: badDate("Non-existent date", s);
182: }
183:
184: } catch (NumberFormatException err) {
185: badDate("Non-numeric component", s);
186: }
187: }
188:
189: private void badDate(String msg, CharSequence value)
190: throws ValidationException {
191: ValidationException err = new ValidationException(
192: "Invalid date " + Err.wrap(value, Err.VALUE) + ". "
193: + msg);
194: err.setErrorCode("FORG0001");
195: throw err;
196: }
197:
198: /**
199: * Get the year component of the date (in local form)
200: */
201:
202: public int getYear() {
203: return year;
204: }
205:
206: /**
207: * Get the month component of the date (in local form)
208: */
209:
210: public byte getMonth() {
211: return month;
212: }
213:
214: /**
215: * Get the day component of the date (in local form)
216: */
217:
218: public byte getDay() {
219: return day;
220: }
221:
222: /**
223: * Test whether a candidate date is actually a valid date in the proleptic Gregorian calendar
224: */
225:
226: private static byte[] daysPerMonth = { 31, 28, 31, 30, 31, 30, 31,
227: 31, 30, 31, 30, 31 };
228:
229: public static boolean isValidDate(int year, int month, int day) {
230: if (month > 0 && month <= 12 && day > 0
231: && day <= daysPerMonth[month - 1]) {
232: return true;
233: }
234: if (month == 2 && day == 29) {
235: return isLeapYear(year);
236: }
237: return false;
238: }
239:
240: /**
241: * Test whether a year is a leap year
242: */
243:
244: public static boolean isLeapYear(int year) {
245: return (year % 4 == 0)
246: && !(year % 100 == 0 && !(year % 400 == 0));
247: }
248:
249: /**
250: * Get the date that immediately follows a given date
251: * @return a new DateValue with no timezone information
252: */
253:
254: public static DateValue tomorrow(int year, byte month, byte day) {
255: if (DateValue.isValidDate(year, month, day + 1)) {
256: return new DateValue(year, month, (byte) (day + 1));
257: } else if (month < 12) {
258: return new DateValue(year, (byte) (month + 1), (byte) 1);
259: } else {
260: return new DateValue(year + 1, (byte) 1, (byte) 1);
261: }
262: }
263:
264: /**
265: * Get the date that immediately precedes a given date
266: * @return a new DateValue with no timezone information
267: */
268:
269: public static DateValue yesterday(int year, byte month, byte day) {
270: if (day > 1) {
271: return new DateValue(year, month, (byte) (day - 1));
272: } else if (month > 1) {
273: if (month == 3 && isLeapYear(year)) {
274: return new DateValue(year, (byte) 2, (byte) 29);
275: } else {
276: return new DateValue(year, (byte) (month - 1),
277: daysPerMonth[month - 2]);
278: }
279: } else {
280: return new DateValue(year - 1, (byte) 12, (byte) 31);
281: }
282: }
283:
284: /**
285: * Convert to target data type
286: * @param requiredType an integer identifying the required atomic type
287: * @param context
288: * @return an AtomicValue, a value of the required type; or an ErrorValue
289: */
290:
291: public AtomicValue convertPrimitive(BuiltInAtomicType requiredType,
292: boolean validate, XPathContext context) {
293: switch (requiredType.getPrimitiveType()) {
294: case Type.DATE:
295: case Type.ANY_ATOMIC:
296: case Type.ITEM:
297: return this ;
298: case Type.DATE_TIME:
299: return toDateTime();
300:
301: case Type.STRING:
302: return new StringValue(getStringValueCS());
303:
304: case Type.UNTYPED_ATOMIC:
305: return new UntypedAtomicValue(getStringValueCS());
306:
307: case Type.G_YEAR: {
308: return new GYearValue(year, getTimezoneInMinutes());
309: }
310: case Type.G_YEAR_MONTH: {
311: return new GYearMonthValue(year, month,
312: getTimezoneInMinutes());
313: }
314: case Type.G_MONTH: {
315: return new GMonthValue(month, getTimezoneInMinutes());
316: }
317: case Type.G_MONTH_DAY: {
318: return new GMonthDayValue(month, day,
319: getTimezoneInMinutes());
320: }
321: case Type.G_DAY: {
322: return new GDayValue(day, getTimezoneInMinutes());
323: }
324:
325: default:
326: ValidationException err = new ValidationException(
327: "Cannot convert date to "
328: + requiredType.getDisplayName());
329: err.setErrorCode("XPTY0004");
330: err.setIsTypeError(true);
331: return new ValidationErrorValue(err);
332: }
333: }
334:
335: /**
336: * Convert to DateTime
337: */
338:
339: public DateTimeValue toDateTime() {
340: return new DateTimeValue(year, month, day, (byte) 0, (byte) 0,
341: (byte) 0, 0, getTimezoneInMinutes());
342: }
343:
344: /**
345: * Convert to string
346: * @return ISO 8601 representation.
347: */
348:
349: public CharSequence getStringValueCS() {
350:
351: FastStringBuffer sb = new FastStringBuffer(16);
352: int yr = year;
353: if (year <= 0) {
354: sb.append('-');
355: yr = -yr + 1; // no year zero in lexical space
356: }
357: appendString(sb, yr, (yr > 9999 ? (yr + "").length() : 4));
358: sb.append('-');
359: appendTwoDigits(sb, month);
360: sb.append('-');
361: appendTwoDigits(sb, day);
362:
363: if (hasTimezone()) {
364: appendTimezone(sb);
365: }
366:
367: return sb;
368:
369: }
370:
371: public GregorianCalendar getCalendar() {
372:
373: int tz = (hasTimezone() ? getTimezoneInMinutes() : 0);
374: TimeZone zone = new SimpleTimeZone(tz * 60000, "LLL");
375: GregorianCalendar calendar = new GregorianCalendar(zone);
376: calendar.clear();
377: calendar.setLenient(false);
378: calendar.set(Math.abs(year), month - 1, day);
379: calendar.set(Calendar.ZONE_OFFSET, tz * 60000);
380: calendar.set(Calendar.DST_OFFSET, 0);
381: if (year < 0) {
382: calendar.set(Calendar.ERA, GregorianCalendar.BC);
383: }
384: calendar.getTime();
385: return calendar;
386: }
387:
388: /**
389: * Determine the data type of the expression
390: * @return Type.DATE_TYPE,
391: * @param th
392: */
393:
394: public ItemType getItemType(TypeHierarchy th) {
395: return Type.DATE_TYPE;
396: }
397:
398: /**
399: * Make a copy of this date, time, or dateTime value
400: */
401:
402: public CalendarValue copy() {
403: return new DateValue(year, month, day, getTimezoneInMinutes());
404: }
405:
406: /**
407: * Return a new date with the same normalized value, but
408: * in a different timezone. This is called only for a DateValue that has an explicit timezone
409: * @param timezone the new timezone offset, in minutes
410: * @return the time in the new timezone. This will be a new TimeValue unless no change
411: * was required to the original value
412: */
413:
414: public CalendarValue adjustTimezone(int timezone) {
415: DateTimeValue dt = (DateTimeValue) toDateTime().adjustTimezone(
416: timezone);
417: return new DateValue(dt.getYear(), dt.getMonth(), dt.getDay(),
418: dt.getTimezoneInMinutes());
419: }
420:
421: /**
422: * Convert to Java object (for passing to external functions)
423: */
424:
425: public Object convertToJava(Class target, XPathContext context)
426: throws XPathException {
427: if (target.isAssignableFrom(Date.class)) {
428: return getCalendar().getTime();
429: } else if (target.isAssignableFrom(GregorianCalendar.class)) {
430: return getCalendar();
431: } else if (target.isAssignableFrom(DateTimeValue.class)) {
432: return this ;
433: } else if (target == String.class
434: || target == CharSequence.class) {
435: return getStringValue();
436: } else if (target == Object.class) {
437: return getStringValue();
438: } else {
439: Object o = super .convertToJava(target, context);
440: if (o == null) {
441: throw new DynamicError("Conversion of date to "
442: + target.getName() + " is not supported");
443: }
444: return o;
445: }
446: }
447:
448: /**
449: * Get a component of the value. Returns null if the timezone component is
450: * requested and is not present.
451: */
452:
453: public AtomicValue getComponent(int component)
454: throws XPathException {
455: switch (component) {
456: case Component.YEAR:
457: return new IntegerValue((year > 0 ? year : year - 1));
458: case Component.MONTH:
459: return new IntegerValue(month);
460: case Component.DAY:
461: return new IntegerValue(day);
462: case Component.TIMEZONE:
463: if (hasTimezone()) {
464: return SecondsDurationValue
465: .fromMilliseconds(getTimezoneInMinutes() * 60000);
466: } else {
467: return null;
468: }
469: default:
470: throw new IllegalArgumentException(
471: "Unknown component for date: " + component);
472: }
473: }
474:
475: /**
476: * Compare the value to another date value. This method is used only during schema processing,
477: * and uses XML Schema semantics rather than XPath semantics.
478: * @param other The other date value. Must be an object of class DateValue.
479: * @return negative value if this one is the earlier, 0 if they are chronologically equal,
480: * positive value if this one is the later. For this purpose, dateTime values with an unknown
481: * timezone are considered to be UTC values (the Comparable interface requires
482: * a total ordering).
483: * @throws ClassCastException if the other value is not a DateValue (the parameter
484: * is declared as Object to satisfy the Comparable interface)
485: */
486:
487: public int compareTo(Object other) {
488: if (!(other instanceof DateValue)) {
489: throw new ClassCastException(
490: "Date values are not comparable to "
491: + other.getClass());
492: }
493: return compareTo((DateValue) other, new IndependentContext()
494: .makeEarlyEvaluationContext());
495: }
496:
497: /**
498: * Compare this value to another value of the same type, using the supplied context object
499: * to get the implicit timezone if required. This method implements the XPath comparison semantics.
500: */
501:
502: public int compareTo(CalendarValue other, XPathContext context) {
503: final TypeHierarchy th = context.getNamePool()
504: .getTypeHierarchy();
505: if (this .getItemType(th).getPrimitiveType() != other
506: .getItemType(th).getPrimitiveType()) {
507: throw new ClassCastException(
508: "Cannot compare values of different types");
509: // covers, for example, comparing a gYear to a gYearMonth
510: }
511: // This code allows comparison of a gYear (etc) to a date, but this is prevented at a higher level
512: return toDateTime().compareTo(other.toDateTime(), context);
513: }
514:
515: public boolean equals(Object other) {
516: return compareTo(other) == 0;
517: }
518:
519: public int hashCode() {
520: // Equality must imply same hashcode, but not vice-versa
521: return getCalendar().getTime().hashCode()
522: + getTimezoneInMinutes();
523: }
524:
525: /**
526: * Add a duration to a date
527: * @param duration the duration to be added (may be negative)
528: * @return the new date
529: * @throws net.sf.saxon.trans.XPathException if the duration is an xs:duration, as distinct from
530: * a subclass thereof
531: */
532:
533: public CalendarValue add(DurationValue duration)
534: throws XPathException {
535: if (duration instanceof SecondsDurationValue) {
536: long microseconds = ((SecondsDurationValue) duration)
537: .getLengthInMicroseconds();
538: boolean negative = (microseconds < 0);
539: microseconds = Math.abs(microseconds);
540: int days = (int) Math.floor((double) microseconds
541: / (1000000L * 60L * 60L * 24L));
542: boolean partDay = (microseconds % (1000000L * 60L * 60L * 24L)) > 0;
543: int julian = getJulianDayNumber(year, month, day);
544: DateValue d = dateFromJulianDayNumber(julian
545: + (negative ? -days : days));
546: if (partDay) {
547: if (negative) {
548: d = yesterday(d.year, d.month, d.day);
549: }
550: }
551: d.setTimezoneInMinutes(getTimezoneInMinutes());
552: return d;
553: } else if (duration instanceof MonthDurationValue) {
554: int months = ((MonthDurationValue) duration)
555: .getLengthInMonths();
556: int m = (month - 1) + months;
557: int y = year + m / 12;
558: m = m % 12;
559: if (m < 0) {
560: m += 12;
561: y -= 1;
562: }
563: m++;
564: int d = day;
565: while (!isValidDate(y, m, d)) {
566: d -= 1;
567: }
568: return new DateValue(y, (byte) m, (byte) d,
569: getTimezoneInMinutes());
570: } else {
571: DynamicError err = new DynamicError(
572: "Date arithmetic is not supported on xs:duration, only on its subtypes");
573: err.setIsTypeError(true);
574: err.setErrorCode("XPTY0004");
575: throw err;
576: }
577: }
578:
579: /**
580: * Determine the difference between two points in time, as a duration
581: * @param other the other point in time
582: * @param context
583: * @return the duration as an xdt:dayTimeDuration
584: * @throws net.sf.saxon.trans.XPathException for example if one value is a date and the other is a time
585: */
586:
587: public SecondsDurationValue subtract(CalendarValue other,
588: XPathContext context) throws XPathException {
589: if (!(other instanceof DateValue)) {
590: DynamicError err = new DynamicError(
591: "First operand of '-' is a date, but the second is not");
592: err.setIsTypeError(true);
593: err.setErrorCode("XPTY0004");
594: throw err;
595: }
596: return super .subtract(other, context);
597: }
598:
599: /**
600: * Calculate the Julian day number at 00:00 on a given date. This algorithm is taken from
601: * http://vsg.cape.com/~pbaum/date/jdalg.htm and
602: * http://vsg.cape.com/~pbaum/date/jdalg2.htm
603: * (adjusted to handle BC dates correctly)
604: */
605:
606: public static int getJulianDayNumber(int year, int month, int day) {
607: int z = year - (month < 3 ? 1 : 0);
608: short f = monthData[month - 1];
609: if (z >= 0) {
610: return day + f + 365 * z + z / 4 - z / 100 + z / 400
611: + 1721118;
612: } else {
613: // for negative years, add 12000 years and then subtract the days!
614: z += 12000;
615: int j = day + f + 365 * z + z / 4 - z / 100 + z / 400
616: + 1721118;
617: return j
618: - (365 * 12000 + 12000 / 4 - 12000 / 100 + 12000 / 400); // number of leap years in 12000 years
619: }
620: }
621:
622: /**
623: * Get the Gregorian date corresponding to a particular Julian day number. The algorithm
624: * is taken from http://www.hermetic.ch/cal_stud/jdn.htm#comp
625: * @return a DateValue with no timezone information set
626: */
627:
628: public static DateValue dateFromJulianDayNumber(int julianDayNumber) {
629: if (julianDayNumber >= 0) {
630: int L = julianDayNumber + 68569 + 1; // +1 adjustment for days starting at noon
631: int n = (4 * L) / 146097;
632: L = L - (146097 * n + 3) / 4;
633: int i = (4000 * (L + 1)) / 1461001;
634: L = L - (1461 * i) / 4 + 31;
635: int j = (80 * L) / 2447;
636: int d = L - (2447 * j) / 80;
637: L = j / 11;
638: int m = j + 2 - (12 * L);
639: int y = 100 * (n - 49) + i + L;
640: return new DateValue(y, (byte) m, (byte) d);
641: } else {
642: // add 12000 years and subtract them again...
643: DateValue dt = dateFromJulianDayNumber(julianDayNumber
644: + (365 * 12000 + 12000 / 4 - 12000 / 100 + 12000 / 400));
645: dt.year -= 12000;
646: return dt;
647: }
648: }
649:
650: private static final short[] monthData = { 306, 337, 0, 31, 61, 92,
651: 122, 153, 184, 214, 245, 275 };
652:
653: /**
654: * Get the ordinal day number within the year (1 Jan = 1, 1 Feb = 32, etc)
655: */
656:
657: public static final int getDayWithinYear(int year, int month,
658: int day) {
659: int j = getJulianDayNumber(year, month, day);
660: int k = getJulianDayNumber(year, 1, 1);
661: return j - k + 1;
662: }
663:
664: /**
665: * Get the day of the week. The days of the week are numbered from
666: * 1 (Monday) to 7 (Sunday)
667: */
668:
669: public static final int getDayOfWeek(int year, int month, int day) {
670: int d = getJulianDayNumber(year, month, day);
671: d -= 2378500; // 1800-01-05 - any Monday would do
672: while (d <= 0) {
673: d += 70000000; // any sufficiently-high multiple of 7 would do
674: }
675: return (d - 1) % 7 + 1;
676: }
677:
678: /**
679: * Get the ISO week number for a given date. The days of the week are numbered from
680: * 1 (Monday) to 7 (Sunday), and week 1 in any calendar year is the week (from Monday to Sunday)
681: * that includes the first Thursday of that year
682: */
683:
684: public static final int getWeekNumber(int year, int month, int day) {
685: int d = getDayWithinYear(year, month, day);
686: int firstDay = getDayOfWeek(year, 1, 1);
687: if (firstDay > 4 && (firstDay + d) <= 8) {
688: // days before week one are part of the last week of the previous year (52 or 53)
689: return getWeekNumber(year - 1, 12, 31);
690: }
691: int inc = (firstDay < 5 ? 1 : 0); // implements the First Thursday rule
692: return ((d + firstDay - 2) / 7) + inc;
693:
694: }
695:
696: /**
697: * Get the week number within a month. This is required for the XSLT format-date() function,
698: * and the rules are not entirely clear. The days of the week are numbered from
699: * 1 (Monday) to 7 (Sunday), and by analogy with the ISO week number, we consider that week 1
700: * in any calendar month is the week (from Monday to Sunday) that includes the first Thursday
701: * of that month. Unlike the ISO week number, we put the previous days in week zero.
702: */
703:
704: public static final int getWeekNumberWithinMonth(int year,
705: int month, int day) {
706: int firstDay = getDayOfWeek(year, month, 1);
707: int inc = (firstDay < 5 ? 1 : 0); // implements the First Thursday rule
708: return ((day + firstDay - 2) / 7) + inc;
709: }
710:
711: /**
712: * Temporary test rig
713: */
714:
715: public static void main(String[] args) throws Exception {
716: DateValue date = new DateValue(args[0]);
717: System.out.println(date.getStringValue());
718: int jd = getJulianDayNumber(date.year, date.month, date.day);
719: System.out.println(jd);
720: System.out
721: .println(dateFromJulianDayNumber(jd).getStringValue());
722: }
723:
724: }
725:
726: //
727: // The contents of this file are subject to the Mozilla Public License Version 1.0 (the "License");
728: // you may not use this file except in compliance with the License. You may obtain a copy of the
729: // License at http://www.mozilla.org/MPL/
730: //
731: // Software distributed under the License is distributed on an "AS IS" basis,
732: // WITHOUT WARRANTY OF ANY KIND, either express or implied.
733: // See the License for the specific language governing rights and limitations under the License.
734: //
735: // The Original Code is: all this file.
736: //
737: // The Initial Developer of the Original Code is Michael H. Kay
738: //
739: // Portions created by (your name) are Copyright (C) (your legal entity). All Rights Reserved.
740: //
741: // Contributor(s): none.
742: //
|