1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.joda.time.tz;
17
18 import java.io.BufferedReader;
19 import java.io.DataOutputStream;
20 import java.io.File;
21 import java.io.FileInputStream;
22 import java.io.FileOutputStream;
23 import java.io.FileReader;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.io.OutputStream;
27 import java.util.ArrayList;
28 import java.util.HashMap;
29 import java.util.List;
30 import java.util.Locale;
31 import java.util.Map;
32 import java.util.StringTokenizer;
33 import java.util.TreeMap;
34 import java.util.Map.Entry;
35
36 import org.joda.time.Chronology;
37 import org.joda.time.DateTime;
38 import org.joda.time.DateTimeField;
39 import org.joda.time.DateTimeZone;
40 import org.joda.time.LocalDate;
41 import org.joda.time.MutableDateTime;
42 import org.joda.time.chrono.ISOChronology;
43 import org.joda.time.chrono.LenientChronology;
44 import org.joda.time.format.DateTimeFormatter;
45 import org.joda.time.format.ISODateTimeFormat;
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63 public class ZoneInfoCompiler {
64 static DateTimeOfYear cStartOfYear;
65
66 static Chronology cLenientISO;
67
68 static ThreadLocal<Boolean> cVerbose = new ThreadLocal<Boolean>();
69 static {
70 cVerbose.set(Boolean.FALSE);
71 }
72
73
74
75
76
77 public static boolean verbose() {
78 return cVerbose.get();
79 }
80
81
82
83
84
85
86
87
88
89
90
91
92
93 public static void main(String[] args) throws Exception {
94 if (args.length == 0) {
95 printUsage();
96 return;
97 }
98
99 File inputDir = null;
100 File outputDir = null;
101 boolean verbose = false;
102
103 int i;
104 for (i=0; i<args.length; i++) {
105 try {
106 if ("-src".equals(args[i])) {
107 inputDir = new File(args[++i]);
108 } else if ("-dst".equals(args[i])) {
109 outputDir = new File(args[++i]);
110 } else if ("-verbose".equals(args[i])) {
111 verbose = true;
112 } else if ("-?".equals(args[i])) {
113 printUsage();
114 return;
115 } else {
116 break;
117 }
118 } catch (IndexOutOfBoundsException e) {
119 printUsage();
120 return;
121 }
122 }
123
124 if (i >= args.length) {
125 printUsage();
126 return;
127 }
128
129 File[] sources = new File[args.length - i];
130 for (int j=0; i<args.length; i++,j++) {
131 sources[j] = inputDir == null ? new File(args[i]) : new File(inputDir, args[i]);
132 }
133
134 cVerbose.set(verbose);
135 ZoneInfoCompiler zic = new ZoneInfoCompiler();
136 zic.compile(outputDir, sources);
137 }
138
139 private static void printUsage() {
140 System.out.println("Usage: java org.joda.time.tz.ZoneInfoCompiler <options> <source files>");
141 System.out.println("where possible options include:");
142 System.out.println(" -src <directory> Specify where to read source files");
143 System.out.println(" -dst <directory> Specify where to write generated files");
144 System.out.println(" -verbose Output verbosely (default false)");
145 }
146
147 static DateTimeOfYear getStartOfYear() {
148 if (cStartOfYear == null) {
149 cStartOfYear = new DateTimeOfYear();
150 }
151 return cStartOfYear;
152 }
153
154 static Chronology getLenientISOChronology() {
155 if (cLenientISO == null) {
156 cLenientISO = LenientChronology.getInstance(ISOChronology.getInstanceUTC());
157 }
158 return cLenientISO;
159 }
160
161
162
163
164 static void writeZoneInfoMap(DataOutputStream dout, Map<String, DateTimeZone> zimap) throws IOException {
165
166 Map<String, Short> idToIndex = new HashMap<String, Short>(zimap.size());
167 TreeMap<Short, String> indexToId = new TreeMap<Short, String>();
168
169 short count = 0;
170 for (Entry<String, DateTimeZone> entry : zimap.entrySet()) {
171 String id = (String)entry.getKey();
172 if (!idToIndex.containsKey(id)) {
173 Short index = Short.valueOf(count);
174 idToIndex.put(id, index);
175 indexToId.put(index, id);
176 if (++count == 0) {
177 throw new InternalError("Too many time zone ids");
178 }
179 }
180 id = ((DateTimeZone)entry.getValue()).getID();
181 if (!idToIndex.containsKey(id)) {
182 Short index = Short.valueOf(count);
183 idToIndex.put(id, index);
184 indexToId.put(index, id);
185 if (++count == 0) {
186 throw new InternalError("Too many time zone ids");
187 }
188 }
189 }
190
191
192 dout.writeShort(indexToId.size());
193 for (String id : indexToId.values()) {
194 dout.writeUTF(id);
195 }
196
197
198 dout.writeShort(zimap.size());
199 for (Entry<String, DateTimeZone> entry : zimap.entrySet()) {
200 String id = entry.getKey();
201 dout.writeShort(idToIndex.get(id).shortValue());
202 id = entry.getValue().getID();
203 dout.writeShort(idToIndex.get(id).shortValue());
204 }
205 }
206
207 static int parseYear(String str, int def) {
208 str = str.toLowerCase();
209 if (str.equals("minimum") || str.equals("min")) {
210 return Integer.MIN_VALUE;
211 } else if (str.equals("maximum") || str.equals("max")) {
212 return Integer.MAX_VALUE;
213 } else if (str.equals("only")) {
214 return def;
215 }
216 return Integer.parseInt(str);
217 }
218
219 static int parseMonth(String str) {
220 DateTimeField field = ISOChronology.getInstanceUTC().monthOfYear();
221 return field.get(field.set(0, str, Locale.ENGLISH));
222 }
223
224 static int parseDayOfWeek(String str) {
225 DateTimeField field = ISOChronology.getInstanceUTC().dayOfWeek();
226 return field.get(field.set(0, str, Locale.ENGLISH));
227 }
228
229 static String parseOptional(String str) {
230 return (str.equals("-")) ? null : str;
231 }
232
233 static int parseTime(String str) {
234 DateTimeFormatter p = ISODateTimeFormat.hourMinuteSecondFraction();
235 MutableDateTime mdt = new MutableDateTime(0, getLenientISOChronology());
236 int pos = 0;
237 if (str.startsWith("-")) {
238 pos = 1;
239 }
240 int newPos = p.parseInto(mdt, str, pos);
241 if (newPos == ~pos) {
242 throw new IllegalArgumentException(str);
243 }
244 int millis = (int)mdt.getMillis();
245 if (pos == 1) {
246 millis = -millis;
247 }
248 return millis;
249 }
250
251 static char parseZoneChar(char c) {
252 switch (c) {
253 case 's': case 'S':
254
255 return 's';
256 case 'u': case 'U': case 'g': case 'G': case 'z': case 'Z':
257
258 return 'u';
259 case 'w': case 'W': default:
260
261 return 'w';
262 }
263 }
264
265
266
267
268 static boolean test(String id, DateTimeZone tz) {
269 if (!id.equals(tz.getID())) {
270 return true;
271 }
272
273
274
275 long millis = ISOChronology.getInstanceUTC().year().set(0, 1850);
276 long end = ISOChronology.getInstanceUTC().year().set(0, 2050);
277
278 int offset = tz.getOffset(millis);
279 String key = tz.getNameKey(millis);
280
281 List<Long> transitions = new ArrayList<Long>();
282
283 while (true) {
284 long next = tz.nextTransition(millis);
285 if (next == millis || next > end) {
286 break;
287 }
288
289 millis = next;
290
291 int nextOffset = tz.getOffset(millis);
292 String nextKey = tz.getNameKey(millis);
293
294 if (offset == nextOffset
295 && key.equals(nextKey)) {
296 System.out.println("*d* Error in " + tz.getID() + " "
297 + new DateTime(millis,
298 ISOChronology.getInstanceUTC()));
299 return false;
300 }
301
302 if (nextKey == null || (nextKey.length() < 3 && !"??".equals(nextKey))) {
303 System.out.println("*s* Error in " + tz.getID() + " "
304 + new DateTime(millis,
305 ISOChronology.getInstanceUTC())
306 + ", nameKey=" + nextKey);
307 return false;
308 }
309
310 transitions.add(Long.valueOf(millis));
311
312 offset = nextOffset;
313 key = nextKey;
314 }
315
316
317
318 millis = ISOChronology.getInstanceUTC().year().set(0, 2050);
319 end = ISOChronology.getInstanceUTC().year().set(0, 1850);
320
321 for (int i=transitions.size(); --i>= 0; ) {
322 long prev = tz.previousTransition(millis);
323 if (prev == millis || prev < end) {
324 break;
325 }
326
327 millis = prev;
328
329 long trans = transitions.get(i).longValue();
330
331 if (trans - 1 != millis) {
332 System.out.println("*r* Error in " + tz.getID() + " "
333 + new DateTime(millis,
334 ISOChronology.getInstanceUTC()) + " != "
335 + new DateTime(trans - 1,
336 ISOChronology.getInstanceUTC()));
337
338 return false;
339 }
340 }
341
342 return true;
343 }
344
345
346 private Map<String, RuleSet> iRuleSets;
347
348
349 private List<Zone> iZones;
350
351
352 private List<String> iLinks;
353
354 public ZoneInfoCompiler() {
355 iRuleSets = new HashMap<String, RuleSet>();
356 iZones = new ArrayList<Zone>();
357 iLinks = new ArrayList<String>();
358 }
359
360
361
362
363
364
365
366 public Map<String, DateTimeZone> compile(File outputDir, File[] sources) throws IOException {
367 if (sources != null) {
368 for (int i=0; i<sources.length; i++) {
369 BufferedReader in = new BufferedReader(new FileReader(sources[i]));
370 parseDataFile(in);
371 in.close();
372 }
373 }
374
375 if (outputDir != null) {
376 if (!outputDir.exists()) {
377 if (!outputDir.mkdirs()) {
378 throw new IOException("Destination directory doesn't exist and cannot be created: " + outputDir);
379 }
380 }
381 if (!outputDir.isDirectory()) {
382 throw new IOException("Destination is not a directory: " + outputDir);
383 }
384 }
385
386 Map<String, DateTimeZone> map = new TreeMap<String, DateTimeZone>();
387
388 System.out.println("Writing zoneinfo files");
389 for (int i=0; i<iZones.size(); i++) {
390 Zone zone = iZones.get(i);
391 DateTimeZoneBuilder builder = new DateTimeZoneBuilder();
392 zone.addToBuilder(builder, iRuleSets);
393 final DateTimeZone original = builder.toDateTimeZone(zone.iName, true);
394 DateTimeZone tz = original;
395 if (test(tz.getID(), tz)) {
396 map.put(tz.getID(), tz);
397 if (outputDir != null) {
398 if (ZoneInfoCompiler.verbose()) {
399 System.out.println("Writing " + tz.getID());
400 }
401 File file = new File(outputDir, tz.getID());
402 if (!file.getParentFile().exists()) {
403 file.getParentFile().mkdirs();
404 }
405 OutputStream out = new FileOutputStream(file);
406 try {
407 builder.writeTo(zone.iName, out);
408 } finally {
409 out.close();
410 }
411
412
413 InputStream in = new FileInputStream(file);
414 DateTimeZone tz2 = DateTimeZoneBuilder.readFrom(in, tz.getID());
415 in.close();
416
417 if (!original.equals(tz2)) {
418 System.out.println("*e* Error in " + tz.getID() +
419 ": Didn't read properly from file");
420 }
421 }
422 }
423 }
424
425 for (int pass=0; pass<2; pass++) {
426 for (int i=0; i<iLinks.size(); i += 2) {
427 String id = iLinks.get(i);
428 String alias = iLinks.get(i + 1);
429 DateTimeZone tz = map.get(id);
430 if (tz == null) {
431 if (pass > 0) {
432 System.out.println("Cannot find time zone '" + id +
433 "' to link alias '" + alias + "' to");
434 }
435 } else {
436 map.put(alias, tz);
437 }
438 }
439 }
440
441 if (outputDir != null) {
442 System.out.println("Writing ZoneInfoMap");
443 File file = new File(outputDir, "ZoneInfoMap");
444 if (!file.getParentFile().exists()) {
445 file.getParentFile().mkdirs();
446 }
447
448 OutputStream out = new FileOutputStream(file);
449 DataOutputStream dout = new DataOutputStream(out);
450 try {
451
452 Map<String, DateTimeZone> zimap = new TreeMap<String, DateTimeZone>(String.CASE_INSENSITIVE_ORDER);
453 zimap.putAll(map);
454 writeZoneInfoMap(dout, zimap);
455 } finally {
456 dout.close();
457 }
458 }
459
460 return map;
461 }
462
463 public void parseDataFile(BufferedReader in) throws IOException {
464 Zone zone = null;
465 String line;
466 while ((line = in.readLine()) != null) {
467 String trimmed = line.trim();
468 if (trimmed.length() == 0 || trimmed.charAt(0) == '#') {
469 continue;
470 }
471
472 int index = line.indexOf('#');
473 if (index >= 0) {
474 line = line.substring(0, index);
475 }
476
477
478
479 StringTokenizer st = new StringTokenizer(line, " \t");
480
481 if (Character.isWhitespace(line.charAt(0)) && st.hasMoreTokens()) {
482 if (zone != null) {
483
484 zone.chain(st);
485 }
486 continue;
487 } else {
488 if (zone != null) {
489 iZones.add(zone);
490 }
491 zone = null;
492 }
493
494 if (st.hasMoreTokens()) {
495 String token = st.nextToken();
496 if (token.equalsIgnoreCase("Rule")) {
497 Rule r = new Rule(st);
498 RuleSet rs = iRuleSets.get(r.iName);
499 if (rs == null) {
500 rs = new RuleSet(r);
501 iRuleSets.put(r.iName, rs);
502 } else {
503 rs.addRule(r);
504 }
505 } else if (token.equalsIgnoreCase("Zone")) {
506 zone = new Zone(st);
507 } else if (token.equalsIgnoreCase("Link")) {
508 iLinks.add(st.nextToken());
509 iLinks.add(st.nextToken());
510 } else {
511 System.out.println("Unknown line: " + line);
512 }
513 }
514 }
515
516 if (zone != null) {
517 iZones.add(zone);
518 }
519 }
520
521 static class DateTimeOfYear {
522 public final int iMonthOfYear;
523 public final int iDayOfMonth;
524 public final int iDayOfWeek;
525 public final boolean iAdvanceDayOfWeek;
526 public final int iMillisOfDay;
527 public final char iZoneChar;
528
529 DateTimeOfYear() {
530 iMonthOfYear = 1;
531 iDayOfMonth = 1;
532 iDayOfWeek = 0;
533 iAdvanceDayOfWeek = false;
534 iMillisOfDay = 0;
535 iZoneChar = 'w';
536 }
537
538 DateTimeOfYear(StringTokenizer st) {
539 int month = 1;
540 int day = 1;
541 int dayOfWeek = 0;
542 int millis = 0;
543 boolean advance = false;
544 char zoneChar = 'w';
545
546 if (st.hasMoreTokens()) {
547 month = parseMonth(st.nextToken());
548
549 if (st.hasMoreTokens()) {
550 String str = st.nextToken();
551 if (str.startsWith("last")) {
552 day = -1;
553 dayOfWeek = parseDayOfWeek(str.substring(4));
554 advance = false;
555 } else {
556 try {
557 day = Integer.parseInt(str);
558 dayOfWeek = 0;
559 advance = false;
560 } catch (NumberFormatException e) {
561 int index = str.indexOf(">=");
562 if (index > 0) {
563 day = Integer.parseInt(str.substring(index + 2));
564 dayOfWeek = parseDayOfWeek(str.substring(0, index));
565 advance = true;
566 } else {
567 index = str.indexOf("<=");
568 if (index > 0) {
569 day = Integer.parseInt(str.substring(index + 2));
570 dayOfWeek = parseDayOfWeek(str.substring(0, index));
571 advance = false;
572 } else {
573 throw new IllegalArgumentException(str);
574 }
575 }
576 }
577 }
578
579 if (st.hasMoreTokens()) {
580 str = st.nextToken();
581 zoneChar = parseZoneChar(str.charAt(str.length() - 1));
582 if (str.equals("24:00")) {
583 LocalDate date = (day == -1 ?
584 new LocalDate(2001, month, 1).plusMonths(1) :
585 new LocalDate(2001, month, day).plusDays(1));
586 advance = (day != -1);
587 month = date.getMonthOfYear();
588 day = date.getDayOfMonth();
589 dayOfWeek = ((dayOfWeek - 1 + 1) % 7) + 1;
590 } else {
591 millis = parseTime(str);
592 }
593 }
594 }
595 }
596
597 iMonthOfYear = month;
598 iDayOfMonth = day;
599 iDayOfWeek = dayOfWeek;
600 iAdvanceDayOfWeek = advance;
601 iMillisOfDay = millis;
602 iZoneChar = zoneChar;
603 }
604
605
606
607
608 public void addRecurring(DateTimeZoneBuilder builder, String nameKey,
609 int saveMillis, int fromYear, int toYear)
610 {
611 builder.addRecurringSavings(nameKey, saveMillis,
612 fromYear, toYear,
613 iZoneChar,
614 iMonthOfYear,
615 iDayOfMonth,
616 iDayOfWeek,
617 iAdvanceDayOfWeek,
618 iMillisOfDay);
619 }
620
621
622
623
624 public void addCutover(DateTimeZoneBuilder builder, int year) {
625 builder.addCutover(year,
626 iZoneChar,
627 iMonthOfYear,
628 iDayOfMonth,
629 iDayOfWeek,
630 iAdvanceDayOfWeek,
631 iMillisOfDay);
632 }
633
634 public String toString() {
635 return
636 "MonthOfYear: " + iMonthOfYear + "\n" +
637 "DayOfMonth: " + iDayOfMonth + "\n" +
638 "DayOfWeek: " + iDayOfWeek + "\n" +
639 "AdvanceDayOfWeek: " + iAdvanceDayOfWeek + "\n" +
640 "MillisOfDay: " + iMillisOfDay + "\n" +
641 "ZoneChar: " + iZoneChar + "\n";
642 }
643 }
644
645 private static class Rule {
646 public final String iName;
647 public final int iFromYear;
648 public final int iToYear;
649 public final String iType;
650 public final DateTimeOfYear iDateTimeOfYear;
651 public final int iSaveMillis;
652 public final String iLetterS;
653
654 Rule(StringTokenizer st) {
655 iName = st.nextToken().intern();
656 iFromYear = parseYear(st.nextToken(), 0);
657 iToYear = parseYear(st.nextToken(), iFromYear);
658 if (iToYear < iFromYear) {
659 throw new IllegalArgumentException();
660 }
661 iType = parseOptional(st.nextToken());
662 iDateTimeOfYear = new DateTimeOfYear(st);
663 iSaveMillis = parseTime(st.nextToken());
664 iLetterS = parseOptional(st.nextToken());
665 }
666
667
668
669
670 public void addRecurring(DateTimeZoneBuilder builder, String nameFormat) {
671 String nameKey = formatName(nameFormat);
672 iDateTimeOfYear.addRecurring
673 (builder, nameKey, iSaveMillis, iFromYear, iToYear);
674 }
675
676 private String formatName(String nameFormat) {
677 int index = nameFormat.indexOf('/');
678 if (index > 0) {
679 if (iSaveMillis == 0) {
680
681 return nameFormat.substring(0, index).intern();
682 } else {
683 return nameFormat.substring(index + 1).intern();
684 }
685 }
686 index = nameFormat.indexOf("%s");
687 if (index < 0) {
688 return nameFormat;
689 }
690 String left = nameFormat.substring(0, index);
691 String right = nameFormat.substring(index + 2);
692 String name;
693 if (iLetterS == null) {
694 name = left.concat(right);
695 } else {
696 name = left + iLetterS + right;
697 }
698 return name.intern();
699 }
700
701 public String toString() {
702 return
703 "[Rule]\n" +
704 "Name: " + iName + "\n" +
705 "FromYear: " + iFromYear + "\n" +
706 "ToYear: " + iToYear + "\n" +
707 "Type: " + iType + "\n" +
708 iDateTimeOfYear +
709 "SaveMillis: " + iSaveMillis + "\n" +
710 "LetterS: " + iLetterS + "\n";
711 }
712 }
713
714 private static class RuleSet {
715 private List<Rule> iRules;
716
717 RuleSet(Rule rule) {
718 iRules = new ArrayList<Rule>();
719 iRules.add(rule);
720 }
721
722 void addRule(Rule rule) {
723 if (!(rule.iName.equals(iRules.get(0).iName))) {
724 throw new IllegalArgumentException("Rule name mismatch");
725 }
726 iRules.add(rule);
727 }
728
729
730
731
732 public void addRecurring(DateTimeZoneBuilder builder, String nameFormat) {
733 for (int i=0; i<iRules.size(); i++) {
734 Rule rule = iRules.get(i);
735 rule.addRecurring(builder, nameFormat);
736 }
737 }
738 }
739
740 private static class Zone {
741 public final String iName;
742 public final int iOffsetMillis;
743 public final String iRules;
744 public final String iFormat;
745 public final int iUntilYear;
746 public final DateTimeOfYear iUntilDateTimeOfYear;
747
748 private Zone iNext;
749
750 Zone(StringTokenizer st) {
751 this(st.nextToken(), st);
752 }
753
754 private Zone(String name, StringTokenizer st) {
755 iName = name.intern();
756 iOffsetMillis = parseTime(st.nextToken());
757 iRules = parseOptional(st.nextToken());
758 iFormat = st.nextToken().intern();
759
760 int year = Integer.MAX_VALUE;
761 DateTimeOfYear dtOfYear = getStartOfYear();
762
763 if (st.hasMoreTokens()) {
764 year = Integer.parseInt(st.nextToken());
765 if (st.hasMoreTokens()) {
766 dtOfYear = new DateTimeOfYear(st);
767 }
768 }
769
770 iUntilYear = year;
771 iUntilDateTimeOfYear = dtOfYear;
772 }
773
774 void chain(StringTokenizer st) {
775 if (iNext != null) {
776 iNext.chain(st);
777 } else {
778 iNext = new Zone(iName, st);
779 }
780 }
781
782
783
784
785
786
787
788
789
790
791
792
793 public void addToBuilder(DateTimeZoneBuilder builder, Map<String, RuleSet> ruleSets) {
794 addToBuilder(this, builder, ruleSets);
795 }
796
797 private static void addToBuilder(Zone zone,
798 DateTimeZoneBuilder builder,
799 Map<String, RuleSet> ruleSets)
800 {
801 for (; zone != null; zone = zone.iNext) {
802 builder.setStandardOffset(zone.iOffsetMillis);
803
804 if (zone.iRules == null) {
805 builder.setFixedSavings(zone.iFormat, 0);
806 } else {
807 try {
808
809 int saveMillis = parseTime(zone.iRules);
810 builder.setFixedSavings(zone.iFormat, saveMillis);
811 }
812 catch (Exception e) {
813 RuleSet rs = ruleSets.get(zone.iRules);
814 if (rs == null) {
815 throw new IllegalArgumentException
816 ("Rules not found: " + zone.iRules);
817 }
818 rs.addRecurring(builder, zone.iFormat);
819 }
820 }
821
822 if (zone.iUntilYear == Integer.MAX_VALUE) {
823 break;
824 }
825
826 zone.iUntilDateTimeOfYear.addCutover(builder, zone.iUntilYear);
827 }
828 }
829
830 public String toString() {
831 String str =
832 "[Zone]\n" +
833 "Name: " + iName + "\n" +
834 "OffsetMillis: " + iOffsetMillis + "\n" +
835 "Rules: " + iRules + "\n" +
836 "Format: " + iFormat + "\n" +
837 "UntilYear: " + iUntilYear + "\n" +
838 iUntilDateTimeOfYear;
839
840 if (iNext == null) {
841 return str;
842 }
843
844 return str + "...\n" + iNext.toString();
845 }
846 }
847 }
848