001 /* 002 * Copyright 2001-2013 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 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 // Test if it can be read back. 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 // Sort and filter out any duplicates that match case. 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 //System.out.println(line); 478 479 StringTokenizer st = new StringTokenizer(line, " \t"); 480 481 if (Character.isWhitespace(line.charAt(0)) && st.hasMoreTokens()) { 482 if (zone != null) { 483 // Zone continuation 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 * Adds a recurring savings rule to the builder. 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 * Adds a cutover to the builder. 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 * Adds a recurring savings rule to the builder. 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 // Extract standard name. 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 * Adds recurring savings rules to the builder. 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 public DateTimeZone buildDateTimeZone(Map ruleSets) { 784 DateTimeZoneBuilder builder = new DateTimeZoneBuilder(); 785 addToBuilder(builder, ruleSets); 786 return builder.toDateTimeZone(iName); 787 } 788 */ 789 790 /** 791 * Adds zone info to the builder. 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 // Check if iRules actually just refers to a savings. 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