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