001 /* 002 * Copyright 2001-2010 Stephen Colebourne 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 package org.joda.time.tz; 017 018 import java.io.BufferedReader; 019 import java.io.DataOutputStream; 020 import java.io.File; 021 import java.io.FileInputStream; 022 import java.io.FileOutputStream; 023 import java.io.FileReader; 024 import java.io.IOException; 025 import java.io.InputStream; 026 import java.io.OutputStream; 027 import java.util.ArrayList; 028 import java.util.HashMap; 029 import java.util.List; 030 import java.util.Locale; 031 import java.util.Map; 032 import java.util.StringTokenizer; 033 import java.util.TreeMap; 034 import java.util.Map.Entry; 035 036 import org.joda.time.Chronology; 037 import org.joda.time.DateTime; 038 import org.joda.time.DateTimeField; 039 import org.joda.time.DateTimeZone; 040 import org.joda.time.LocalDate; 041 import org.joda.time.MutableDateTime; 042 import org.joda.time.chrono.ISOChronology; 043 import org.joda.time.chrono.LenientChronology; 044 import org.joda.time.format.DateTimeFormatter; 045 import org.joda.time.format.ISODateTimeFormat; 046 047 /** 048 * Compiles Olson ZoneInfo database files into binary files for each time zone 049 * in the database. {@link DateTimeZoneBuilder} is used to construct and encode 050 * compiled data files. {@link ZoneInfoProvider} loads the encoded files and 051 * converts them back into {@link DateTimeZone} objects. 052 * <p> 053 * Although this tool is similar to zic, the binary formats are not 054 * compatible. The latest Olson database files may be obtained 055 * <a href="http://www.twinsun.com/tz/tz-link.htm">here</a>. 056 * <p> 057 * ZoneInfoCompiler is mutable and not thread-safe, although the main method 058 * may be safely invoked by multiple threads. 059 * 060 * @author Brian S O'Neill 061 * @since 1.0 062 */ 063 public class ZoneInfoCompiler { 064 static DateTimeOfYear cStartOfYear; 065 066 static Chronology cLenientISO; 067 068 static ThreadLocal<Boolean> cVerbose = new ThreadLocal<Boolean>(); 069 static { 070 cVerbose.set(Boolean.FALSE); 071 } 072 073 /** 074 * Gets a flag indicating that verbose logging is required. 075 * @return true to log verbosely 076 */ 077 public static boolean verbose() { 078 return cVerbose.get(); 079 } 080 081 //----------------------------------------------------------------------- 082 /** 083 * Launches the ZoneInfoCompiler tool. 084 * 085 * <pre> 086 * Usage: java org.joda.time.tz.ZoneInfoCompiler <options> <source files> 087 * where possible options include: 088 * -src <directory> Specify where to read source files 089 * -dst <directory> Specify where to write generated files 090 * -verbose Output verbosely (default false) 091 * </pre> 092 */ 093 public static void main(String[] args) throws Exception { 094 if (args.length == 0) { 095 printUsage(); 096 return; 097 } 098 099 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 * @param zimap maps string ids to DateTimeZone objects. 163 */ 164 static void writeZoneInfoMap(DataOutputStream dout, Map<String, DateTimeZone> zimap) throws IOException { 165 // Build the string pool. 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 // Write the string pool, ordered by index. 192 dout.writeShort(indexToId.size()); 193 for (String id : indexToId.values()) { 194 dout.writeUTF(id); 195 } 196 197 // Write the mappings. 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 // Standard time 255 return 's'; 256 case 'u': case 'U': case 'g': case 'G': case 'z': case 'Z': 257 // UTC 258 return 'u'; 259 case 'w': case 'W': default: 260 // Wall time 261 return 'w'; 262 } 263 } 264 265 /** 266 * @return false if error. 267 */ 268 static boolean test(String id, DateTimeZone tz) { 269 if (!id.equals(tz.getID())) { 270 return true; 271 } 272 273 // Test to ensure that reported transitions are not duplicated. 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 // Now verify that reverse transitions match up. 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 // Maps names to RuleSets. 346 private Map<String, RuleSet> iRuleSets; 347 348 // List of Zone objects. 349 private List<Zone> iZones; 350 351 // List String pairs to link. 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 * Returns a map of ids to DateTimeZones. 362 * 363 * @param outputDir optional directory to write compiled data files to 364 * @param sources optional list of source files to parse 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 throw new IOException("Destination directory doesn't exist: " + outputDir); 378 } 379 if (!outputDir.isDirectory()) { 380 throw new IOException("Destination is not a directory: " + outputDir); 381 } 382 } 383 384 Map<String, DateTimeZone> map = new TreeMap<String, DateTimeZone>(); 385 386 System.out.println("Writing zoneinfo files"); 387 for (int i=0; i<iZones.size(); i++) { 388 Zone zone = iZones.get(i); 389 DateTimeZoneBuilder builder = new DateTimeZoneBuilder(); 390 zone.addToBuilder(builder, iRuleSets); 391 final DateTimeZone original = builder.toDateTimeZone(zone.iName, true); 392 DateTimeZone tz = original; 393 if (test(tz.getID(), tz)) { 394 map.put(tz.getID(), tz); 395 if (outputDir != null) { 396 if (ZoneInfoCompiler.verbose()) { 397 System.out.println("Writing " + tz.getID()); 398 } 399 File file = new File(outputDir, tz.getID()); 400 if (!file.getParentFile().exists()) { 401 file.getParentFile().mkdirs(); 402 } 403 OutputStream out = new FileOutputStream(file); 404 try { 405 builder.writeTo(zone.iName, out); 406 } finally { 407 out.close(); 408 } 409 410 // Test if it can be read back. 411 InputStream in = new FileInputStream(file); 412 DateTimeZone tz2 = DateTimeZoneBuilder.readFrom(in, tz.getID()); 413 in.close(); 414 415 if (!original.equals(tz2)) { 416 System.out.println("*e* Error in " + tz.getID() + 417 ": Didn't read properly from file"); 418 } 419 } 420 } 421 } 422 423 for (int pass=0; pass<2; pass++) { 424 for (int i=0; i<iLinks.size(); i += 2) { 425 String id = iLinks.get(i); 426 String alias = iLinks.get(i + 1); 427 DateTimeZone tz = map.get(id); 428 if (tz == null) { 429 if (pass > 0) { 430 System.out.println("Cannot find time zone '" + id + 431 "' to link alias '" + alias + "' to"); 432 } 433 } else { 434 map.put(alias, tz); 435 } 436 } 437 } 438 439 if (outputDir != null) { 440 System.out.println("Writing ZoneInfoMap"); 441 File file = new File(outputDir, "ZoneInfoMap"); 442 if (!file.getParentFile().exists()) { 443 file.getParentFile().mkdirs(); 444 } 445 446 OutputStream out = new FileOutputStream(file); 447 DataOutputStream dout = new DataOutputStream(out); 448 try { 449 // Sort and filter out any duplicates that match case. 450 Map<String, DateTimeZone> zimap = new TreeMap<String, DateTimeZone>(String.CASE_INSENSITIVE_ORDER); 451 zimap.putAll(map); 452 writeZoneInfoMap(dout, zimap); 453 } finally { 454 dout.close(); 455 } 456 } 457 458 return map; 459 } 460 461 public void parseDataFile(BufferedReader in) throws IOException { 462 Zone zone = null; 463 String line; 464 while ((line = in.readLine()) != null) { 465 String trimmed = line.trim(); 466 if (trimmed.length() == 0 || trimmed.charAt(0) == '#') { 467 continue; 468 } 469 470 int index = line.indexOf('#'); 471 if (index >= 0) { 472 line = line.substring(0, index); 473 } 474 475 //System.out.println(line); 476 477 StringTokenizer st = new StringTokenizer(line, " \t"); 478 479 if (Character.isWhitespace(line.charAt(0)) && st.hasMoreTokens()) { 480 if (zone != null) { 481 // Zone continuation 482 zone.chain(st); 483 } 484 continue; 485 } else { 486 if (zone != null) { 487 iZones.add(zone); 488 } 489 zone = null; 490 } 491 492 if (st.hasMoreTokens()) { 493 String token = st.nextToken(); 494 if (token.equalsIgnoreCase("Rule")) { 495 Rule r = new Rule(st); 496 RuleSet rs = iRuleSets.get(r.iName); 497 if (rs == null) { 498 rs = new RuleSet(r); 499 iRuleSets.put(r.iName, rs); 500 } else { 501 rs.addRule(r); 502 } 503 } else if (token.equalsIgnoreCase("Zone")) { 504 zone = new Zone(st); 505 } else if (token.equalsIgnoreCase("Link")) { 506 iLinks.add(st.nextToken()); 507 iLinks.add(st.nextToken()); 508 } else { 509 System.out.println("Unknown line: " + line); 510 } 511 } 512 } 513 514 if (zone != null) { 515 iZones.add(zone); 516 } 517 } 518 519 static class DateTimeOfYear { 520 public final int iMonthOfYear; 521 public final int iDayOfMonth; 522 public final int iDayOfWeek; 523 public final boolean iAdvanceDayOfWeek; 524 public final int iMillisOfDay; 525 public final char iZoneChar; 526 527 DateTimeOfYear() { 528 iMonthOfYear = 1; 529 iDayOfMonth = 1; 530 iDayOfWeek = 0; 531 iAdvanceDayOfWeek = false; 532 iMillisOfDay = 0; 533 iZoneChar = 'w'; 534 } 535 536 DateTimeOfYear(StringTokenizer st) { 537 int month = 1; 538 int day = 1; 539 int dayOfWeek = 0; 540 int millis = 0; 541 boolean advance = false; 542 char zoneChar = 'w'; 543 544 if (st.hasMoreTokens()) { 545 month = parseMonth(st.nextToken()); 546 547 if (st.hasMoreTokens()) { 548 String str = st.nextToken(); 549 if (str.startsWith("last")) { 550 day = -1; 551 dayOfWeek = parseDayOfWeek(str.substring(4)); 552 advance = false; 553 } else { 554 try { 555 day = Integer.parseInt(str); 556 dayOfWeek = 0; 557 advance = false; 558 } catch (NumberFormatException e) { 559 int index = str.indexOf(">="); 560 if (index > 0) { 561 day = Integer.parseInt(str.substring(index + 2)); 562 dayOfWeek = parseDayOfWeek(str.substring(0, index)); 563 advance = true; 564 } else { 565 index = str.indexOf("<="); 566 if (index > 0) { 567 day = Integer.parseInt(str.substring(index + 2)); 568 dayOfWeek = parseDayOfWeek(str.substring(0, index)); 569 advance = false; 570 } else { 571 throw new IllegalArgumentException(str); 572 } 573 } 574 } 575 } 576 577 if (st.hasMoreTokens()) { 578 str = st.nextToken(); 579 zoneChar = parseZoneChar(str.charAt(str.length() - 1)); 580 if (str.equals("24:00")) { 581 LocalDate date = (day == -1 ? 582 new LocalDate(2001, month, 1).plusMonths(1) : 583 new LocalDate(2001, month, day).plusDays(1)); 584 advance = (day != -1); 585 month = date.getMonthOfYear(); 586 day = date.getDayOfMonth(); 587 dayOfWeek = ((dayOfWeek - 1 + 1) % 7) + 1; 588 } else { 589 millis = parseTime(str); 590 } 591 } 592 } 593 } 594 595 iMonthOfYear = month; 596 iDayOfMonth = day; 597 iDayOfWeek = dayOfWeek; 598 iAdvanceDayOfWeek = advance; 599 iMillisOfDay = millis; 600 iZoneChar = zoneChar; 601 } 602 603 /** 604 * Adds a recurring savings rule to the builder. 605 */ 606 public void addRecurring(DateTimeZoneBuilder builder, String nameKey, 607 int saveMillis, int fromYear, int toYear) 608 { 609 builder.addRecurringSavings(nameKey, saveMillis, 610 fromYear, toYear, 611 iZoneChar, 612 iMonthOfYear, 613 iDayOfMonth, 614 iDayOfWeek, 615 iAdvanceDayOfWeek, 616 iMillisOfDay); 617 } 618 619 /** 620 * Adds a cutover to the builder. 621 */ 622 public void addCutover(DateTimeZoneBuilder builder, int year) { 623 builder.addCutover(year, 624 iZoneChar, 625 iMonthOfYear, 626 iDayOfMonth, 627 iDayOfWeek, 628 iAdvanceDayOfWeek, 629 iMillisOfDay); 630 } 631 632 public String toString() { 633 return 634 "MonthOfYear: " + iMonthOfYear + "\n" + 635 "DayOfMonth: " + iDayOfMonth + "\n" + 636 "DayOfWeek: " + iDayOfWeek + "\n" + 637 "AdvanceDayOfWeek: " + iAdvanceDayOfWeek + "\n" + 638 "MillisOfDay: " + iMillisOfDay + "\n" + 639 "ZoneChar: " + iZoneChar + "\n"; 640 } 641 } 642 643 private static class Rule { 644 public final String iName; 645 public final int iFromYear; 646 public final int iToYear; 647 public final String iType; 648 public final DateTimeOfYear iDateTimeOfYear; 649 public final int iSaveMillis; 650 public final String iLetterS; 651 652 Rule(StringTokenizer st) { 653 iName = st.nextToken().intern(); 654 iFromYear = parseYear(st.nextToken(), 0); 655 iToYear = parseYear(st.nextToken(), iFromYear); 656 if (iToYear < iFromYear) { 657 throw new IllegalArgumentException(); 658 } 659 iType = parseOptional(st.nextToken()); 660 iDateTimeOfYear = new DateTimeOfYear(st); 661 iSaveMillis = parseTime(st.nextToken()); 662 iLetterS = parseOptional(st.nextToken()); 663 } 664 665 /** 666 * Adds a recurring savings rule to the builder. 667 */ 668 public void addRecurring(DateTimeZoneBuilder builder, String nameFormat) { 669 String nameKey = formatName(nameFormat); 670 iDateTimeOfYear.addRecurring 671 (builder, nameKey, iSaveMillis, iFromYear, iToYear); 672 } 673 674 private String formatName(String nameFormat) { 675 int index = nameFormat.indexOf('/'); 676 if (index > 0) { 677 if (iSaveMillis == 0) { 678 // Extract standard name. 679 return nameFormat.substring(0, index).intern(); 680 } else { 681 return nameFormat.substring(index + 1).intern(); 682 } 683 } 684 index = nameFormat.indexOf("%s"); 685 if (index < 0) { 686 return nameFormat; 687 } 688 String left = nameFormat.substring(0, index); 689 String right = nameFormat.substring(index + 2); 690 String name; 691 if (iLetterS == null) { 692 name = left.concat(right); 693 } else { 694 name = left + iLetterS + right; 695 } 696 return name.intern(); 697 } 698 699 public String toString() { 700 return 701 "[Rule]\n" + 702 "Name: " + iName + "\n" + 703 "FromYear: " + iFromYear + "\n" + 704 "ToYear: " + iToYear + "\n" + 705 "Type: " + iType + "\n" + 706 iDateTimeOfYear + 707 "SaveMillis: " + iSaveMillis + "\n" + 708 "LetterS: " + iLetterS + "\n"; 709 } 710 } 711 712 private static class RuleSet { 713 private List<Rule> iRules; 714 715 RuleSet(Rule rule) { 716 iRules = new ArrayList<Rule>(); 717 iRules.add(rule); 718 } 719 720 void addRule(Rule rule) { 721 if (!(rule.iName.equals(iRules.get(0).iName))) { 722 throw new IllegalArgumentException("Rule name mismatch"); 723 } 724 iRules.add(rule); 725 } 726 727 /** 728 * Adds recurring savings rules to the builder. 729 */ 730 public void addRecurring(DateTimeZoneBuilder builder, String nameFormat) { 731 for (int i=0; i<iRules.size(); i++) { 732 Rule rule = iRules.get(i); 733 rule.addRecurring(builder, nameFormat); 734 } 735 } 736 } 737 738 private static class Zone { 739 public final String iName; 740 public final int iOffsetMillis; 741 public final String iRules; 742 public final String iFormat; 743 public final int iUntilYear; 744 public final DateTimeOfYear iUntilDateTimeOfYear; 745 746 private Zone iNext; 747 748 Zone(StringTokenizer st) { 749 this(st.nextToken(), st); 750 } 751 752 private Zone(String name, StringTokenizer st) { 753 iName = name.intern(); 754 iOffsetMillis = parseTime(st.nextToken()); 755 iRules = parseOptional(st.nextToken()); 756 iFormat = st.nextToken().intern(); 757 758 int year = Integer.MAX_VALUE; 759 DateTimeOfYear dtOfYear = getStartOfYear(); 760 761 if (st.hasMoreTokens()) { 762 year = Integer.parseInt(st.nextToken()); 763 if (st.hasMoreTokens()) { 764 dtOfYear = new DateTimeOfYear(st); 765 } 766 } 767 768 iUntilYear = year; 769 iUntilDateTimeOfYear = dtOfYear; 770 } 771 772 void chain(StringTokenizer st) { 773 if (iNext != null) { 774 iNext.chain(st); 775 } else { 776 iNext = new Zone(iName, st); 777 } 778 } 779 780 /* 781 public DateTimeZone buildDateTimeZone(Map ruleSets) { 782 DateTimeZoneBuilder builder = new DateTimeZoneBuilder(); 783 addToBuilder(builder, ruleSets); 784 return builder.toDateTimeZone(iName); 785 } 786 */ 787 788 /** 789 * Adds zone info to the builder. 790 */ 791 public void addToBuilder(DateTimeZoneBuilder builder, Map<String, RuleSet> ruleSets) { 792 addToBuilder(this, builder, ruleSets); 793 } 794 795 private static void addToBuilder(Zone zone, 796 DateTimeZoneBuilder builder, 797 Map<String, RuleSet> ruleSets) 798 { 799 for (; zone != null; zone = zone.iNext) { 800 builder.setStandardOffset(zone.iOffsetMillis); 801 802 if (zone.iRules == null) { 803 builder.setFixedSavings(zone.iFormat, 0); 804 } else { 805 try { 806 // Check if iRules actually just refers to a savings. 807 int saveMillis = parseTime(zone.iRules); 808 builder.setFixedSavings(zone.iFormat, saveMillis); 809 } 810 catch (Exception e) { 811 RuleSet rs = ruleSets.get(zone.iRules); 812 if (rs == null) { 813 throw new IllegalArgumentException 814 ("Rules not found: " + zone.iRules); 815 } 816 rs.addRecurring(builder, zone.iFormat); 817 } 818 } 819 820 if (zone.iUntilYear == Integer.MAX_VALUE) { 821 break; 822 } 823 824 zone.iUntilDateTimeOfYear.addCutover(builder, zone.iUntilYear); 825 } 826 } 827 828 public String toString() { 829 String str = 830 "[Zone]\n" + 831 "Name: " + iName + "\n" + 832 "OffsetMillis: " + iOffsetMillis + "\n" + 833 "Rules: " + iRules + "\n" + 834 "Format: " + iFormat + "\n" + 835 "UntilYear: " + iUntilYear + "\n" + 836 iUntilDateTimeOfYear; 837 838 if (iNext == null) { 839 return str; 840 } 841 842 return str + "...\n" + iNext.toString(); 843 } 844 } 845 } 846