source: trunk/gcc/libjava/java/text/SimpleDateFormat.java

Last change on this file was 2, checked in by bird, 22 years ago

Initial revision

  • Property cvs2svn:cvs-rev set to 1.1
  • Property svn:eol-style set to native
  • Property svn:executable set to *
File size: 21.5 KB
Line 
1/* SimpleDateFormat.java -- A class for parsing/formating simple
2 date constructs
3 Copyright (C) 1998, 1999, 2000, 2001 Free Software Foundation, Inc.
4
5This file is part of GNU Classpath.
6
7GNU Classpath is free software; you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation; either version 2, or (at your option)
10any later version.
11
12GNU Classpath is distributed in the hope that it will be useful, but
13WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Classpath; see the file COPYING. If not, write to the
19Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
2002111-1307 USA.
21
22Linking this library statically or dynamically with other modules is
23making a combined work based on this library. Thus, the terms and
24conditions of the GNU General Public License cover the whole
25combination.
26
27As a special exception, the copyright holders of this library give you
28permission to link this library with independent modules to produce an
29executable, regardless of the license terms of these independent
30modules, and to copy and distribute the resulting executable under
31terms of your choice, provided that you also meet, for each linked
32independent module, the terms and conditions of the license of that
33module. An independent module is a module which is not derived from
34or based on this library. If you modify this library, you may extend
35this exception to your version of the library, but you are not
36obligated to do so. If you do not wish to do so, delete this
37exception statement from your version. */
38
39
40package java.text;
41
42import java.util.Calendar;
43import java.util.Date;
44import java.util.Enumeration;
45import java.util.GregorianCalendar;
46import java.util.Locale;
47import java.util.TimeZone;
48import java.util.SimpleTimeZone;
49import java.util.Vector;
50import java.io.ObjectInputStream;
51import java.io.IOException;
52
53/**
54 * SimpleDateFormat provides convenient methods for parsing and formatting
55 * dates using Gregorian calendars (see java.util.GregorianCalendar).
56 */
57public class SimpleDateFormat extends DateFormat
58{
59 /** A pair class used by SimpleDateFormat as a compiled representation
60 * of a format string.
61 */
62 private class FieldSizePair
63 {
64 public int field;
65 public int size;
66
67 /** Constructs a pair with the given field and size values */
68 public FieldSizePair(int f, int s) {
69 field = f;
70 size = s;
71 }
72 }
73
74 private transient Vector tokens;
75 private DateFormatSymbols formatData; // formatData
76 private Date defaultCenturyStart;
77 private transient int defaultCentury;
78 private String pattern;
79 private int serialVersionOnStream = 1; // 0 indicates JDK1.1.3 or earlier
80 private static final long serialVersionUID = 4774881970558875024L;
81
82 // This string is specified in the JCL. We set it here rather than
83 // do a DateFormatSymbols(Locale.US).getLocalPatternChars() since
84 // someone could theoretically change those values (though unlikely).
85 private static final String standardChars = "GyMdkHmsSEDFwWahKz";
86
87 private void readObject(ObjectInputStream stream)
88 throws IOException, ClassNotFoundException
89 {
90 stream.defaultReadObject();
91 if (serialVersionOnStream < 1)
92 {
93 computeCenturyStart ();
94 serialVersionOnStream = 1;
95 }
96 else
97 // Ensure that defaultCentury gets set.
98 set2DigitYearStart(defaultCenturyStart);
99
100 // Set up items normally taken care of by the constructor.
101 tokens = new Vector();
102 compileFormat(pattern);
103 }
104
105 private void compileFormat(String pattern)
106 {
107 // Any alphabetical characters are treated as pattern characters
108 // unless enclosed in single quotes.
109
110 char thisChar;
111 int pos;
112 int field;
113 FieldSizePair current = null;
114
115 for (int i=0; i<pattern.length(); i++) {
116 thisChar = pattern.charAt(i);
117 field = formatData.getLocalPatternChars().indexOf(thisChar);
118 if (field == -1) {
119 current = null;
120 if (Character.isLetter(thisChar)) {
121 // Not a valid letter
122 tokens.addElement(new FieldSizePair(-1,0));
123 } else if (thisChar == '\'') {
124 // Quoted text section; skip to next single quote
125 pos = pattern.indexOf('\'',i+1);
126 if (pos == -1) {
127 // This ought to be an exception, but spec does not
128 // let us throw one.
129 tokens.addElement(new FieldSizePair(-1,0));
130 }
131 if ((pos+1 < pattern.length()) && (pattern.charAt(pos+1) == '\'')) {
132 tokens.addElement(pattern.substring(i+1,pos+1));
133 } else {
134 tokens.addElement(pattern.substring(i+1,pos));
135 }
136 i = pos;
137 } else {
138 // A special character
139 tokens.addElement(new Character(thisChar));
140 }
141 } else {
142 // A valid field
143 if ((current != null) && (field == current.field)) {
144 current.size++;
145 } else {
146 current = new FieldSizePair(field,1);
147 tokens.addElement(current);
148 }
149 }
150 }
151 }
152
153 public String toString()
154 {
155 StringBuffer output = new StringBuffer();
156 Enumeration e = tokens.elements();
157 while (e.hasMoreElements()) {
158 output.append(e.nextElement().toString());
159 }
160 return output.toString();
161 }
162
163 /**
164 * Constructs a SimpleDateFormat using the default pattern for
165 * the default locale.
166 */
167 public SimpleDateFormat()
168 {
169 /*
170 * There does not appear to be a standard API for determining
171 * what the default pattern for a locale is, so use package-scope
172 * variables in DateFormatSymbols to encapsulate this.
173 */
174 super();
175 Locale locale = Locale.getDefault();
176 calendar = new GregorianCalendar(locale);
177 computeCenturyStart();
178 tokens = new Vector();
179 formatData = new DateFormatSymbols(locale);
180 pattern = (formatData.dateFormats[DEFAULT] + ' '
181 + formatData.timeFormats[DEFAULT]);
182 compileFormat(pattern);
183 numberFormat = NumberFormat.getInstance(locale);
184 numberFormat.setGroupingUsed (false);
185 }
186
187 /**
188 * Creates a date formatter using the specified pattern, with the default
189 * DateFormatSymbols for the default locale.
190 */
191 public SimpleDateFormat(String pattern)
192 {
193 this(pattern, Locale.getDefault());
194 }
195
196 /**
197 * Creates a date formatter using the specified pattern, with the default
198 * DateFormatSymbols for the given locale.
199 */
200 public SimpleDateFormat(String pattern, Locale locale)
201 {
202 super();
203 calendar = new GregorianCalendar(locale);
204 computeCenturyStart();
205 tokens = new Vector();
206 formatData = new DateFormatSymbols(locale);
207 compileFormat(pattern);
208 this.pattern = pattern;
209 numberFormat = NumberFormat.getInstance(locale);
210 numberFormat.setGroupingUsed (false);
211 }
212
213 /**
214 * Creates a date formatter using the specified pattern. The
215 * specified DateFormatSymbols will be used when formatting.
216 */
217 public SimpleDateFormat(String pattern, DateFormatSymbols formatData)
218 {
219 super();
220 calendar = new GregorianCalendar();
221 computeCenturyStart ();
222 tokens = new Vector();
223 this.formatData = formatData;
224 compileFormat(pattern);
225 this.pattern = pattern;
226 numberFormat = NumberFormat.getInstance();
227 numberFormat.setGroupingUsed (false);
228 }
229
230 // What is the difference between localized and unlocalized? The
231 // docs don't say.
232
233 /**
234 * This method returns a string with the formatting pattern being used
235 * by this object. This string is unlocalized.
236 *
237 * @return The format string.
238 */
239 public String toPattern()
240 {
241 return pattern;
242 }
243
244 /**
245 * This method returns a string with the formatting pattern being used
246 * by this object. This string is localized.
247 *
248 * @return The format string.
249 */
250 public String toLocalizedPattern()
251 {
252 String localChars = formatData.getLocalPatternChars();
253 return applyLocalizedPattern (pattern, standardChars, localChars);
254 }
255
256 /**
257 * This method sets the formatting pattern that should be used by this
258 * object. This string is not localized.
259 *
260 * @param pattern The new format pattern.
261 */
262 public void applyPattern(String pattern)
263 {
264 tokens = new Vector();
265 compileFormat(pattern);
266 this.pattern = pattern;
267 }
268
269 /**
270 * This method sets the formatting pattern that should be used by this
271 * object. This string is localized.
272 *
273 * @param pattern The new format pattern.
274 */
275 public void applyLocalizedPattern(String pattern)
276 {
277 String localChars = formatData.getLocalPatternChars();
278 pattern = applyLocalizedPattern (pattern, localChars, standardChars);
279 applyPattern(pattern);
280 }
281
282 private String applyLocalizedPattern(String pattern,
283 String oldChars, String newChars)
284 {
285 int len = pattern.length();
286 StringBuffer buf = new StringBuffer(len);
287 boolean quoted = false;
288 for (int i = 0; i < len; i++)
289 {
290 char ch = pattern.charAt(i);
291 if (ch == '\'')
292 quoted = ! quoted;
293 if (! quoted)
294 {
295 int j = oldChars.indexOf(ch);
296 if (j >= 0)
297 ch = newChars.charAt(j);
298 }
299 buf.append(ch);
300 }
301 return buf.toString();
302 }
303
304 /**
305 * Returns the start of the century used for two digit years.
306 *
307 * @return A <code>Date</code> representing the start of the century
308 * for two digit years.
309 */
310 public Date get2DigitYearStart()
311 {
312 return defaultCenturyStart;
313 }
314
315 /**
316 * Sets the start of the century used for two digit years.
317 *
318 * @param date A <code>Date</code> representing the start of the century for
319 * two digit years.
320 */
321 public void set2DigitYearStart(Date date)
322 {
323 defaultCenturyStart = date;
324 calendar.clear();
325 calendar.setTime(date);
326 int year = calendar.get(Calendar.YEAR);
327 defaultCentury = year - (year % 100);
328 }
329
330 /**
331 * This method returns the format symbol information used for parsing
332 * and formatting dates.
333 *
334 * @return The date format symbols.
335 */
336 public DateFormatSymbols getDateFormatSymbols()
337 {
338 return formatData;
339 }
340
341 /**
342 * This method sets the format symbols information used for parsing
343 * and formatting dates.
344 *
345 * @param formatData The date format symbols.
346 */
347 public void setDateFormatSymbols(DateFormatSymbols formatData)
348 {
349 this.formatData = formatData;
350 }
351
352 /**
353 * This methods tests whether the specified object is equal to this
354 * object. This will be true if and only if the specified object:
355 * <p>
356 * <ul>
357 * <li>Is not <code>null</code>.
358 * <li>Is an instance of <code>SimpleDateFormat</code>.
359 * <li>Is equal to this object at the superclass (i.e., <code>DateFormat</code>)
360 * level.
361 * <li>Has the same formatting pattern.
362 * <li>Is using the same formatting symbols.
363 * <li>Is using the same century for two digit years.
364 * </ul>
365 *
366 * @param obj The object to compare for equality against.
367 *
368 * @return <code>true</code> if the specified object is equal to this object,
369 * <code>false</code> otherwise.
370 */
371 public boolean equals(Object o)
372 {
373 if (o == null)
374 return false;
375
376 if (!super.equals(o))
377 return false;
378
379 if (!(o instanceof SimpleDateFormat))
380 return false;
381
382 SimpleDateFormat sdf = (SimpleDateFormat)o;
383
384 if (!toPattern().equals(sdf.toPattern()))
385 return false;
386
387 if (!get2DigitYearStart().equals(sdf.get2DigitYearStart()))
388 return false;
389
390 if (!getDateFormatSymbols().equals(sdf.getDateFormatSymbols()))
391 return false;
392
393 return true;
394 }
395
396
397 /**
398 * Formats the date input according to the format string in use,
399 * appending to the specified StringBuffer. The input StringBuffer
400 * is returned as output for convenience.
401 */
402 public StringBuffer format(Date date, StringBuffer buffer, FieldPosition pos)
403 {
404 String temp;
405 calendar.setTime(date);
406
407 // go through vector, filling in fields where applicable, else toString
408 Enumeration e = tokens.elements();
409 while (e.hasMoreElements()) {
410 Object o = e.nextElement();
411 if (o instanceof FieldSizePair) {
412 FieldSizePair p = (FieldSizePair) o;
413 int beginIndex = buffer.length();
414 switch (p.field) {
415 case ERA_FIELD:
416 buffer.append(formatData.eras[calendar.get(Calendar.ERA)]);
417 break;
418 case YEAR_FIELD:
419 temp = String.valueOf(calendar.get(Calendar.YEAR));
420 if (p.size < 4)
421 buffer.append(temp.substring(temp.length()-2));
422 else
423 buffer.append(temp);
424 break;
425 case MONTH_FIELD:
426 if (p.size < 3)
427 withLeadingZeros(calendar.get(Calendar.MONTH)+1,p.size,buffer);
428 else if (p.size < 4)
429 buffer.append(formatData.shortMonths[calendar.get(Calendar.MONTH)]);
430 else
431 buffer.append(formatData.months[calendar.get(Calendar.MONTH)]);
432 break;
433 case DATE_FIELD:
434 withLeadingZeros(calendar.get(Calendar.DATE),p.size,buffer);
435 break;
436 case HOUR_OF_DAY1_FIELD: // 1-24
437 withLeadingZeros(((calendar.get(Calendar.HOUR_OF_DAY)+23)%24)+1,p.size,buffer);
438 break;
439 case HOUR_OF_DAY0_FIELD: // 0-23
440 withLeadingZeros(calendar.get(Calendar.HOUR_OF_DAY),p.size,buffer);
441 break;
442 case MINUTE_FIELD:
443 withLeadingZeros(calendar.get(Calendar.MINUTE),p.size,buffer);
444 break;
445 case SECOND_FIELD:
446 withLeadingZeros(calendar.get(Calendar.SECOND),p.size,buffer);
447 break;
448 case MILLISECOND_FIELD:
449 withLeadingZeros(calendar.get(Calendar.MILLISECOND),p.size,buffer);
450 break;
451 case DAY_OF_WEEK_FIELD:
452 if (p.size < 4)
453 buffer.append(formatData.shortWeekdays[calendar.get(Calendar.DAY_OF_WEEK)]);
454 else
455 buffer.append(formatData.weekdays[calendar.get(Calendar.DAY_OF_WEEK)]);
456 break;
457 case DAY_OF_YEAR_FIELD:
458 withLeadingZeros(calendar.get(Calendar.DAY_OF_YEAR),p.size,buffer);
459 break;
460 case DAY_OF_WEEK_IN_MONTH_FIELD:
461 withLeadingZeros(calendar.get(Calendar.DAY_OF_WEEK_IN_MONTH),p.size,buffer);
462 break;
463 case WEEK_OF_YEAR_FIELD:
464 withLeadingZeros(calendar.get(Calendar.WEEK_OF_YEAR),p.size,buffer);
465 break;
466 case WEEK_OF_MONTH_FIELD:
467 withLeadingZeros(calendar.get(Calendar.WEEK_OF_MONTH),p.size,buffer);
468 break;
469 case AM_PM_FIELD:
470 buffer.append(formatData.ampms[calendar.get(Calendar.AM_PM)]);
471 break;
472 case HOUR1_FIELD: // 1-12
473 withLeadingZeros(((calendar.get(Calendar.HOUR)+11)%12)+1,p.size,buffer);
474 break;
475 case HOUR0_FIELD: // 0-11
476 withLeadingZeros(calendar.get(Calendar.HOUR),p.size,buffer);
477 break;
478 case TIMEZONE_FIELD:
479 TimeZone zone = calendar.getTimeZone();
480 boolean isDST = calendar.get(Calendar.DST_OFFSET) != 0;
481 // FIXME: XXX: This should be a localized time zone.
482 String zoneID = zone.getDisplayName(isDST, p.size > 3 ? TimeZone.LONG : TimeZone.SHORT);
483 buffer.append(zoneID);
484 break;
485 default:
486 throw new IllegalArgumentException("Illegal pattern character");
487 }
488 if (pos != null && p.field == pos.getField())
489 {
490 pos.setBeginIndex(beginIndex);
491 pos.setEndIndex(buffer.length());
492 }
493 } else {
494 buffer.append(o.toString());
495 }
496 }
497 return buffer;
498 }
499
500 private void withLeadingZeros(int value, int length, StringBuffer buffer)
501 {
502 String valStr = String.valueOf(value);
503 for (length -= valStr.length(); length > 0; length--)
504 buffer.append('0');
505 buffer.append(valStr);
506 }
507
508 private final boolean expect (String source, ParsePosition pos, char ch)
509 {
510 int x = pos.getIndex();
511 boolean r = x < source.length() && source.charAt(x) == ch;
512 if (r)
513 pos.setIndex(x + 1);
514 else
515 pos.setErrorIndex(x);
516 return r;
517 }
518
519 /**
520 * This method parses the specified string into a date.
521 *
522 * @param dateStr The date string to parse.
523 * @param pos The input and output parse position
524 *
525 * @return The parsed date, or <code>null</code> if the string cannot be
526 * parsed.
527 */
528 public Date parse (String dateStr, ParsePosition pos)
529 {
530 int fmt_index = 0;
531 int fmt_max = pattern.length();
532
533 calendar.clear();
534 boolean saw_timezone = false;
535 int quote_start = -1;
536 boolean is2DigitYear = false;
537 for (; fmt_index < fmt_max; ++fmt_index)
538 {
539 char ch = pattern.charAt(fmt_index);
540 if (ch == '\'')
541 {
542 int index = pos.getIndex();
543 if (fmt_index < fmt_max - 1
544 && pattern.charAt(fmt_index + 1) == '\'')
545 {
546 if (! expect (dateStr, pos, ch))
547 return null;
548 ++fmt_index;
549 }
550 else
551 quote_start = quote_start < 0 ? fmt_index : -1;
552 continue;
553 }
554
555 if (quote_start != -1
556 || ((ch < 'a' || ch > 'z')
557 && (ch < 'A' || ch > 'Z')))
558 {
559 if (! expect (dateStr, pos, ch))
560 return null;
561 continue;
562 }
563
564 // We've arrived at a potential pattern character in the
565 // pattern.
566 int first = fmt_index;
567 while (++fmt_index < fmt_max && pattern.charAt(fmt_index) == ch)
568 ;
569 int fmt_count = fmt_index - first;
570 --fmt_index;
571
572 // We can handle most fields automatically: most either are
573 // numeric or are looked up in a string vector. In some cases
574 // we need an offset. When numeric, `offset' is added to the
575 // resulting value. When doing a string lookup, offset is the
576 // initial index into the string array.
577 int calendar_field;
578 boolean is_numeric = true;
579 String[] match = null;
580 int offset = 0;
581 boolean maybe2DigitYear = false;
582 switch (ch)
583 {
584 case 'd':
585 calendar_field = Calendar.DATE;
586 break;
587 case 'D':
588 calendar_field = Calendar.DAY_OF_YEAR;
589 break;
590 case 'F':
591 calendar_field = Calendar.DAY_OF_WEEK_IN_MONTH;
592 break;
593 case 'E':
594 is_numeric = false;
595 offset = 1;
596 calendar_field = Calendar.DAY_OF_WEEK;
597 match = (fmt_count <= 3
598 ? formatData.getShortWeekdays()
599 : formatData.getWeekdays());
600 break;
601 case 'w':
602 calendar_field = Calendar.WEEK_OF_YEAR;
603 break;
604 case 'W':
605 calendar_field = Calendar.WEEK_OF_MONTH;
606 break;
607 case 'M':
608 calendar_field = Calendar.MONTH;
609 if (fmt_count <= 2)
610 offset = -1;
611 else
612 {
613 is_numeric = false;
614 match = (fmt_count <= 3
615 ? formatData.getShortMonths()
616 : formatData.getMonths());
617 }
618 break;
619 case 'y':
620 calendar_field = Calendar.YEAR;
621 if (fmt_count <= 2)
622 maybe2DigitYear = true;
623 break;
624 case 'K':
625 calendar_field = Calendar.HOUR;
626 break;
627 case 'h':
628 calendar_field = Calendar.HOUR;
629 break;
630 case 'H':
631 calendar_field = Calendar.HOUR_OF_DAY;
632 break;
633 case 'k':
634 calendar_field = Calendar.HOUR_OF_DAY;
635 break;
636 case 'm':
637 calendar_field = Calendar.MINUTE;
638 break;
639 case 's':
640 calendar_field = Calendar.SECOND;
641 break;
642 case 'S':
643 calendar_field = Calendar.MILLISECOND;
644 break;
645 case 'a':
646 is_numeric = false;
647 calendar_field = Calendar.AM_PM;
648 match = formatData.getAmPmStrings();
649 break;
650 case 'z':
651 // We need a special case for the timezone, because it
652 // uses a different data structure than the other cases.
653 is_numeric = false;
654 calendar_field = Calendar.DST_OFFSET;
655 String[][] zoneStrings = formatData.getZoneStrings();
656 int zoneCount = zoneStrings.length;
657 int index = pos.getIndex();
658 boolean found_zone = false;
659 for (int j = 0; j < zoneCount; j++)
660 {
661 String[] strings = zoneStrings[j];
662 int k;
663 for (k = 1; k < strings.length; ++k)
664 {
665 if (dateStr.startsWith(strings[k], index))
666 break;
667 }
668 if (k != strings.length)
669 {
670 found_zone = true;
671 saw_timezone = true;
672 TimeZone tz = TimeZone.getTimeZone (strings[0]);
673 calendar.setTimeZone (tz);
674 calendar.set (Calendar.ZONE_OFFSET, tz.getRawOffset ());
675 offset = 0;
676 if (k > 2 && tz instanceof SimpleTimeZone)
677 {
678 SimpleTimeZone stz = (SimpleTimeZone) tz;
679 offset = stz.getDSTSavings ();
680 }
681 pos.setIndex(index + strings[k].length());
682 break;
683 }
684 }
685 if (! found_zone)
686 {
687 pos.setErrorIndex(pos.getIndex());
688 return null;
689 }
690 break;
691 default:
692 pos.setErrorIndex(pos.getIndex());
693 return null;
694 }
695
696 // Compute the value we should assign to the field.
697 int value;
698 int index = -1;
699 if (is_numeric)
700 {
701 numberFormat.setMinimumIntegerDigits(fmt_count);
702 if (maybe2DigitYear)
703 index = pos.getIndex();
704 Number n = numberFormat.parse(dateStr, pos);
705 if (pos == null || ! (n instanceof Long))
706 return null;
707 value = n.intValue() + offset;
708 }
709 else if (match != null)
710 {
711 index = pos.getIndex();
712 int i;
713 for (i = offset; i < match.length; ++i)
714 {
715 if (dateStr.startsWith(match[i], index))
716 break;
717 }
718 if (i == match.length)
719 {
720 pos.setErrorIndex(index);
721 return null;
722 }
723 pos.setIndex(index + match[i].length());
724 value = i;
725 }
726 else
727 value = offset;
728
729 if (maybe2DigitYear)
730 {
731 // Parse into default century if the numeric year string has
732 // exactly 2 digits.
733 int digit_count = pos.getIndex() - index;
734 if (digit_count == 2)
735 is2DigitYear = true;
736 }
737
738 // Assign the value and move on.
739 calendar.set(calendar_field, value);
740 }
741
742 if (is2DigitYear)
743 {
744 // Apply the 80-20 heuristic to dermine the full year based on
745 // defaultCenturyStart.
746 int year = defaultCentury + calendar.get(Calendar.YEAR);
747 calendar.set(Calendar.YEAR, year);
748 if (calendar.getTime().compareTo(defaultCenturyStart) < 0)
749 calendar.set(Calendar.YEAR, year + 100);
750 }
751
752 try
753 {
754 if (! saw_timezone)
755 {
756 // Use the real rules to determine whether or not this
757 // particular time is in daylight savings.
758 calendar.clear (Calendar.DST_OFFSET);
759 calendar.clear (Calendar.ZONE_OFFSET);
760 }
761 return calendar.getTime();
762 }
763 catch (IllegalArgumentException x)
764 {
765 pos.setErrorIndex(pos.getIndex());
766 return null;
767 }
768 }
769
770 // Compute the start of the current century as defined by
771 // get2DigitYearStart.
772 private void computeCenturyStart()
773 {
774 int year = calendar.get(Calendar.YEAR);
775 calendar.set(Calendar.YEAR, year - 80);
776 set2DigitYearStart(calendar.getTime());
777 }
778}
Note: See TracBrowser for help on using the repository browser.