001/** 002 * This software is provided under the terms of the Minecraft Forge Public 003 * License v1.0. 004 */ 005 006package net.minecraftforge.common; 007 008import static net.minecraftforge.common.Property.Type.BOOLEAN; 009import static net.minecraftforge.common.Property.Type.DOUBLE; 010import static net.minecraftforge.common.Property.Type.INTEGER; 011import static net.minecraftforge.common.Property.Type.STRING; 012 013import java.io.BufferedReader; 014import java.io.BufferedWriter; 015import java.io.File; 016import java.io.FileInputStream; 017import java.io.FileOutputStream; 018import java.io.IOException; 019import java.io.InputStream; 020import java.io.InputStreamReader; 021import java.io.OutputStreamWriter; 022import java.io.PushbackInputStream; 023import java.io.Reader; 024import java.util.ArrayList; 025import java.util.Arrays; 026import java.util.Locale; 027import java.util.Map; 028import java.util.Set; 029import java.util.TreeMap; 030import java.util.regex.Matcher; 031import java.util.regex.Pattern; 032 033import net.minecraft.block.Block; 034import net.minecraft.item.Item; 035 036import com.google.common.base.CharMatcher; 037import com.google.common.collect.ImmutableSet; 038 039import cpw.mods.fml.common.FMLLog; 040import cpw.mods.fml.common.Loader; 041import cpw.mods.fml.relauncher.FMLInjectionData; 042 043/** 044 * This class offers advanced configurations capabilities, allowing to provide 045 * various categories for configuration variables. 046 */ 047public class Configuration 048{ 049 private static boolean[] configMarkers = new boolean[Item.itemsList.length]; 050 private static final int ITEM_SHIFT = 256; 051 private static final int MAX_BLOCKS = 4096; 052 053 public static final String CATEGORY_GENERAL = "general"; 054 public static final String CATEGORY_BLOCK = "block"; 055 public static final String CATEGORY_ITEM = "item"; 056 public static final String ALLOWED_CHARS = "._-"; 057 public static final String DEFAULT_ENCODING = "UTF-8"; 058 public static final String CATEGORY_SPLITTER = "."; 059 public static final String NEW_LINE; 060 private static final Pattern CONFIG_START = Pattern.compile("START: \"([^\\\"]+)\""); 061 private static final Pattern CONFIG_END = Pattern.compile("END: \"([^\\\"]+)\""); 062 public static final CharMatcher allowedProperties = CharMatcher.JAVA_LETTER_OR_DIGIT.or(CharMatcher.anyOf(ALLOWED_CHARS)); 063 private static Configuration PARENT = null; 064 065 File file; 066 067 private Map<String, ConfigCategory> categories = new TreeMap<String, ConfigCategory>(); 068 private Map<String, Configuration> children = new TreeMap<String, Configuration>(); 069 070 private boolean caseSensitiveCustomCategories; 071 public String defaultEncoding = DEFAULT_ENCODING; 072 private String fileName = null; 073 public boolean isChild = false; 074 private boolean changed = false; 075 076 static 077 { 078 Arrays.fill(configMarkers, false); 079 NEW_LINE = System.getProperty("line.separator"); 080 } 081 082 public Configuration(){} 083 084 /** 085 * Create a configuration file for the file given in parameter. 086 */ 087 public Configuration(File file) 088 { 089 this.file = file; 090 String basePath = ((File)(FMLInjectionData.data()[6])).getAbsolutePath().replace(File.separatorChar, '/').replace("/.", ""); 091 String path = file.getAbsolutePath().replace(File.separatorChar, '/').replace("/./", "/").replace(basePath, ""); 092 if (PARENT != null) 093 { 094 PARENT.setChild(path, this); 095 isChild = true; 096 } 097 else 098 { 099 fileName = path; 100 load(); 101 } 102 } 103 104 public Configuration(File file, boolean caseSensitiveCustomCategories) 105 { 106 this(file); 107 this.caseSensitiveCustomCategories = caseSensitiveCustomCategories; 108 } 109 110 /** 111 * Gets or create a block id property. If the block id property key is 112 * already in the configuration, then it will be used. Otherwise, 113 * defaultId will be used, except if already taken, in which case this 114 * will try to determine a free default id. 115 */ 116 public Property getBlock(String key, int defaultID) { return getBlock(CATEGORY_BLOCK, key, defaultID, null); } 117 public Property getBlock(String key, int defaultID, String comment) { return getBlock(CATEGORY_BLOCK, key, defaultID, comment); } 118 public Property getBlock(String category, String key, int defaultID) { return getBlockInternal(category, key, defaultID, null, 256, Block.blocksList.length); } 119 public Property getBlock(String category, String key, int defaultID, String comment) { return getBlockInternal(category, key, defaultID, comment, 256, Block.blocksList.length); } 120 121 /** 122 * Special version of getBlock to be used when you want to garentee the ID you get is below 256 123 * This should ONLY be used by mods who do low level terrain generation, or ones that add new 124 * biomes. 125 * EXA: ExtraBiomesXL 126 * 127 * Specifically, if your block is used BEFORE the Chunk is created, and placed in the terrain byte array directly. 128 * If you add a new biome and you set the top/filler block, they need to be <256, nothing else. 129 * 130 * If you're adding a new ore, DON'T call this function. 131 * 132 * Normal mods such as '50 new ores' do not need to be below 256 so should use the normal getBlock 133 */ 134 public Property getTerrainBlock(String category, String key, int defaultID, String comment) 135 { 136 return getBlockInternal(category, key, defaultID, comment, 0, 256); 137 } 138 139 private Property getBlockInternal(String category, String key, int defaultID, String comment, int lower, int upper) 140 { 141 Property prop = get(category, key, -1, comment); 142 143 if (prop.getInt() != -1) 144 { 145 configMarkers[prop.getInt()] = true; 146 return prop; 147 } 148 else 149 { 150 if (defaultID < lower) 151 { 152 FMLLog.warning( 153 "Mod attempted to get a block ID with a default in the Terrain Generation section, " + 154 "mod authors should make sure there defaults are above 256 unless explicitly needed " + 155 "for terrain generation. Most ores do not need to be below 256."); 156 FMLLog.warning("Config \"%s\" Category: \"%s\" Key: \"%s\" Default: %d", fileName, category, key, defaultID); 157 defaultID = upper - 1; 158 } 159 160 if (Block.blocksList[defaultID] == null && !configMarkers[defaultID]) 161 { 162 prop.set(defaultID); 163 configMarkers[defaultID] = true; 164 return prop; 165 } 166 else 167 { 168 for (int j = upper - 1; j > 0; j--) 169 { 170 if (Block.blocksList[j] == null && !configMarkers[j]) 171 { 172 prop.set(j); 173 configMarkers[j] = true; 174 return prop; 175 } 176 } 177 178 throw new RuntimeException("No more block ids available for " + key); 179 } 180 } 181 } 182 183 public Property getItem(String key, int defaultID) { return getItem(CATEGORY_ITEM, key, defaultID, null); } 184 public Property getItem(String key, int defaultID, String comment) { return getItem(CATEGORY_ITEM, key, defaultID, comment); } 185 public Property getItem(String category, String key, int defaultID) { return getItem(category, key, defaultID, null); } 186 187 public Property getItem(String category, String key, int defaultID, String comment) 188 { 189 Property prop = get(category, key, -1, comment); 190 int defaultShift = defaultID + ITEM_SHIFT; 191 192 if (prop.getInt() != -1) 193 { 194 configMarkers[prop.getInt() + ITEM_SHIFT] = true; 195 return prop; 196 } 197 else 198 { 199 if (defaultID < MAX_BLOCKS - ITEM_SHIFT) 200 { 201 FMLLog.warning( 202 "Mod attempted to get a item ID with a default value in the block ID section, " + 203 "mod authors should make sure there defaults are above %d unless explicitly needed " + 204 "so that all block ids are free to store blocks.", MAX_BLOCKS - ITEM_SHIFT); 205 FMLLog.warning("Config \"%s\" Category: \"%s\" Key: \"%s\" Default: %d", fileName, category, key, defaultID); 206 } 207 208 if (Item.itemsList[defaultShift] == null && !configMarkers[defaultShift] && defaultShift >= Block.blocksList.length) 209 { 210 prop.set(defaultID); 211 configMarkers[defaultShift] = true; 212 return prop; 213 } 214 else 215 { 216 for (int x = Item.itemsList.length - 1; x >= ITEM_SHIFT; x--) 217 { 218 if (Item.itemsList[x] == null && !configMarkers[x]) 219 { 220 prop.set(x - ITEM_SHIFT); 221 configMarkers[x] = true; 222 return prop; 223 } 224 } 225 226 throw new RuntimeException("No more item ids available for " + key); 227 } 228 } 229 } 230 231 public Property get(String category, String key, int defaultValue) 232 { 233 return get(category, key, defaultValue, null); 234 } 235 236 public Property get(String category, String key, int defaultValue, String comment) 237 { 238 Property prop = get(category, key, Integer.toString(defaultValue), comment, INTEGER); 239 if (!prop.isIntValue()) 240 { 241 prop.set(defaultValue); 242 } 243 return prop; 244 } 245 246 public Property get(String category, String key, boolean defaultValue) 247 { 248 return get(category, key, defaultValue, null); 249 } 250 251 public Property get(String category, String key, boolean defaultValue, String comment) 252 { 253 Property prop = get(category, key, Boolean.toString(defaultValue), comment, BOOLEAN); 254 if (!prop.isBooleanValue()) 255 { 256 prop.set(defaultValue); 257 } 258 return prop; 259 } 260 261 public Property get(String category, String key, double defaultValue) 262 { 263 return get(category, key, defaultValue, null); 264 } 265 266 public Property get(String category, String key, double defaultValue, String comment) 267 { 268 Property prop = get(category, key, Double.toString(defaultValue), comment, DOUBLE); 269 if (!prop.isDoubleValue()) 270 { 271 prop.set(defaultValue); 272 } 273 return prop; 274 } 275 276 public Property get(String category, String key, String defaultValue) 277 { 278 return get(category, key, defaultValue, null); 279 } 280 281 public Property get(String category, String key, String defaultValue, String comment) 282 { 283 return get(category, key, defaultValue, comment, STRING); 284 } 285 286 public Property get(String category, String key, String[] defaultValue) 287 { 288 return get(category, key, defaultValue, null); 289 } 290 291 public Property get(String category, String key, String[] defaultValue, String comment) 292 { 293 return get(category, key, defaultValue, comment, STRING); 294 } 295 296 public Property get(String category, String key, int[] defaultValue) 297 { 298 return get(category, key, defaultValue, null); 299 } 300 301 public Property get(String category, String key, int[] defaultValue, String comment) 302 { 303 String[] values = new String[defaultValue.length]; 304 for (int i = 0; i < defaultValue.length; i++) 305 { 306 values[i] = Integer.toString(defaultValue[i]); 307 } 308 309 Property prop = get(category, key, values, comment, INTEGER); 310 if (!prop.isIntList()) 311 { 312 prop.set(values); 313 } 314 315 return prop; 316 } 317 318 public Property get(String category, String key, double[] defaultValue) 319 { 320 return get(category, key, defaultValue, null); 321 } 322 323 public Property get(String category, String key, double[] defaultValue, String comment) 324 { 325 String[] values = new String[defaultValue.length]; 326 for (int i = 0; i < defaultValue.length; i++) 327 { 328 values[i] = Double.toString(defaultValue[i]); 329 } 330 331 Property prop = get(category, key, values, comment, DOUBLE); 332 333 if (!prop.isDoubleList()) 334 { 335 prop.set(values); 336 } 337 338 return prop; 339 } 340 341 public Property get(String category, String key, boolean[] defaultValue) 342 { 343 return get(category, key, defaultValue, null); 344 } 345 346 public Property get(String category, String key, boolean[] defaultValue, String comment) 347 { 348 String[] values = new String[defaultValue.length]; 349 for (int i = 0; i < defaultValue.length; i++) 350 { 351 values[i] = Boolean.toString(defaultValue[i]); 352 } 353 354 Property prop = get(category, key, values, comment, BOOLEAN); 355 356 if (!prop.isBooleanList()) 357 { 358 prop.set(values); 359 } 360 361 return prop; 362 } 363 364 public Property get(String category, String key, String defaultValue, String comment, Property.Type type) 365 { 366 if (!caseSensitiveCustomCategories) 367 { 368 category = category.toLowerCase(Locale.ENGLISH); 369 } 370 371 ConfigCategory cat = getCategory(category); 372 373 if (cat.containsKey(key)) 374 { 375 Property prop = cat.get(key); 376 377 if (prop.getType() == null) 378 { 379 prop = new Property(prop.getName(), prop.getString(), type); 380 cat.put(key, prop); 381 } 382 383 prop.comment = comment; 384 return prop; 385 } 386 else if (defaultValue != null) 387 { 388 Property prop = new Property(key, defaultValue, type); 389 prop.set(defaultValue); //Set and mark as dirty to signify it should save 390 cat.put(key, prop); 391 prop.comment = comment; 392 return prop; 393 } 394 else 395 { 396 return null; 397 } 398 } 399 400 public Property get(String category, String key, String[] defaultValue, String comment, Property.Type type) 401 { 402 if (!caseSensitiveCustomCategories) 403 { 404 category = category.toLowerCase(Locale.ENGLISH); 405 } 406 407 ConfigCategory cat = getCategory(category); 408 409 if (cat.containsKey(key)) 410 { 411 Property prop = cat.get(key); 412 413 if (prop.getType() == null) 414 { 415 prop = new Property(prop.getName(), prop.getString(), type); 416 cat.put(key, prop); 417 } 418 419 prop.comment = comment; 420 421 return prop; 422 } 423 else if (defaultValue != null) 424 { 425 Property prop = new Property(key, defaultValue, type); 426 prop.comment = comment; 427 cat.put(key, prop); 428 return prop; 429 } 430 else 431 { 432 return null; 433 } 434 } 435 436 public boolean hasCategory(String category) 437 { 438 return categories.get(category) != null; 439 } 440 441 public boolean hasKey(String category, String key) 442 { 443 ConfigCategory cat = categories.get(category); 444 return cat != null && cat.containsKey(key); 445 } 446 447 public void load() 448 { 449 if (PARENT != null && PARENT != this) 450 { 451 return; 452 } 453 454 BufferedReader buffer = null; 455 UnicodeInputStreamReader input = null; 456 try 457 { 458 if (file.getParentFile() != null) 459 { 460 file.getParentFile().mkdirs(); 461 } 462 463 if (!file.exists() && !file.createNewFile()) 464 { 465 return; 466 } 467 468 if (file.canRead()) 469 { 470 input = new UnicodeInputStreamReader(new FileInputStream(file), defaultEncoding); 471 defaultEncoding = input.getEncoding(); 472 buffer = new BufferedReader(input); 473 474 String line; 475 ConfigCategory currentCat = null; 476 Property.Type type = null; 477 ArrayList<String> tmpList = null; 478 int lineNum = 0; 479 String name = null; 480 481 while (true) 482 { 483 lineNum++; 484 line = buffer.readLine(); 485 486 if (line == null) 487 { 488 break; 489 } 490 491 Matcher start = CONFIG_START.matcher(line); 492 Matcher end = CONFIG_END.matcher(line); 493 494 if (start.matches()) 495 { 496 fileName = start.group(1); 497 categories = new TreeMap<String, ConfigCategory>(); 498 continue; 499 } 500 else if (end.matches()) 501 { 502 fileName = end.group(1); 503 Configuration child = new Configuration(); 504 child.categories = categories; 505 this.children.put(fileName, child); 506 continue; 507 } 508 509 int nameStart = -1, nameEnd = -1; 510 boolean skip = false; 511 boolean quoted = false; 512 513 for (int i = 0; i < line.length() && !skip; ++i) 514 { 515 if (Character.isLetterOrDigit(line.charAt(i)) || ALLOWED_CHARS.indexOf(line.charAt(i)) != -1 || (quoted && line.charAt(i) != '"')) 516 { 517 if (nameStart == -1) 518 { 519 nameStart = i; 520 } 521 522 nameEnd = i; 523 } 524 else if (Character.isWhitespace(line.charAt(i))) 525 { 526 // ignore space charaters 527 } 528 else 529 { 530 switch (line.charAt(i)) 531 { 532 case '#': 533 skip = true; 534 continue; 535 536 case '"': 537 if (quoted) 538 { 539 quoted = false; 540 } 541 if (!quoted && nameStart == -1) 542 { 543 quoted = true; 544 } 545 break; 546 547 case '{': 548 name = line.substring(nameStart, nameEnd + 1); 549 String qualifiedName = ConfigCategory.getQualifiedName(name, currentCat); 550 551 ConfigCategory cat = categories.get(qualifiedName); 552 if (cat == null) 553 { 554 currentCat = new ConfigCategory(name, currentCat); 555 categories.put(qualifiedName, currentCat); 556 } 557 else 558 { 559 currentCat = cat; 560 } 561 name = null; 562 563 break; 564 565 case '}': 566 if (currentCat == null) 567 { 568 throw new RuntimeException(String.format("Config file corrupt, attepted to close to many categories '%s:%d'", fileName, lineNum)); 569 } 570 currentCat = currentCat.parent; 571 break; 572 573 case '=': 574 name = line.substring(nameStart, nameEnd + 1); 575 576 if (currentCat == null) 577 { 578 throw new RuntimeException(String.format("'%s' has no scope in '%s:%d'", name, fileName, lineNum)); 579 } 580 581 Property prop = new Property(name, line.substring(i + 1), type, true); 582 i = line.length(); 583 584 currentCat.put(name, prop); 585 586 break; 587 588 case ':': 589 type = Property.Type.tryParse(line.substring(nameStart, nameEnd + 1).charAt(0)); 590 nameStart = nameEnd = -1; 591 break; 592 593 case '<': 594 if (tmpList != null) 595 { 596 throw new RuntimeException(String.format("Malformed list property \"%s:%d\"", fileName, lineNum)); 597 } 598 599 name = line.substring(nameStart, nameEnd + 1); 600 601 if (currentCat == null) 602 { 603 throw new RuntimeException(String.format("'%s' has no scope in '%s:%d'", name, fileName, lineNum)); 604 } 605 606 tmpList = new ArrayList<String>(); 607 608 skip = true; 609 610 break; 611 612 case '>': 613 if (tmpList == null) 614 { 615 throw new RuntimeException(String.format("Malformed list property \"%s:%d\"", fileName, lineNum)); 616 } 617 618 currentCat.put(name, new Property(name, tmpList.toArray(new String[tmpList.size()]), type)); 619 name = null; 620 tmpList = null; 621 type = null; 622 break; 623 624 default: 625 throw new RuntimeException(String.format("Unknown character '%s' in '%s:%d'", line.charAt(i), fileName, lineNum)); 626 } 627 } 628 } 629 630 if (quoted) 631 { 632 throw new RuntimeException(String.format("Unmatched quote in '%s:%d'", fileName, lineNum)); 633 } 634 else if (tmpList != null && !skip) 635 { 636 tmpList.add(line.trim()); 637 } 638 } 639 } 640 } 641 catch (IOException e) 642 { 643 e.printStackTrace(); 644 } 645 finally 646 { 647 if (buffer != null) 648 { 649 try 650 { 651 buffer.close(); 652 } catch (IOException e){} 653 } 654 if (input != null) 655 { 656 try 657 { 658 input.close(); 659 } catch (IOException e){} 660 } 661 } 662 663 resetChangedState(); 664 } 665 666 public void save() 667 { 668 if (PARENT != null && PARENT != this) 669 { 670 PARENT.save(); 671 return; 672 } 673 674 try 675 { 676 if (file.getParentFile() != null) 677 { 678 file.getParentFile().mkdirs(); 679 } 680 681 if (!file.exists() && !file.createNewFile()) 682 { 683 return; 684 } 685 686 if (file.canWrite()) 687 { 688 FileOutputStream fos = new FileOutputStream(file); 689 BufferedWriter buffer = new BufferedWriter(new OutputStreamWriter(fos, defaultEncoding)); 690 691 buffer.write("# Configuration file" + NEW_LINE + NEW_LINE); 692 693 if (children.isEmpty()) 694 { 695 save(buffer); 696 } 697 else 698 { 699 for (Map.Entry<String, Configuration> entry : children.entrySet()) 700 { 701 buffer.write("START: \"" + entry.getKey() + "\"" + NEW_LINE); 702 entry.getValue().save(buffer); 703 buffer.write("END: \"" + entry.getKey() + "\"" + NEW_LINE + NEW_LINE); 704 } 705 } 706 707 buffer.close(); 708 fos.close(); 709 } 710 } 711 catch (IOException e) 712 { 713 e.printStackTrace(); 714 } 715 } 716 717 private void save(BufferedWriter out) throws IOException 718 { 719 for (ConfigCategory cat : categories.values()) 720 { 721 if (!cat.isChild()) 722 { 723 cat.write(out, 0); 724 out.newLine(); 725 } 726 } 727 } 728 729 public ConfigCategory getCategory(String category) 730 { 731 ConfigCategory ret = categories.get(category); 732 733 if (ret == null) 734 { 735 if (category.contains(CATEGORY_SPLITTER)) 736 { 737 String[] hierarchy = category.split("\\"+CATEGORY_SPLITTER); 738 ConfigCategory parent = categories.get(hierarchy[0]); 739 740 if (parent == null) 741 { 742 parent = new ConfigCategory(hierarchy[0]); 743 categories.put(parent.getQualifiedName(), parent); 744 changed = true; 745 } 746 747 for (int i = 1; i < hierarchy.length; i++) 748 { 749 String name = ConfigCategory.getQualifiedName(hierarchy[i], parent); 750 ConfigCategory child = categories.get(name); 751 752 if (child == null) 753 { 754 child = new ConfigCategory(hierarchy[i], parent); 755 categories.put(name, child); 756 changed = true; 757 } 758 759 ret = child; 760 parent = child; 761 } 762 } 763 else 764 { 765 ret = new ConfigCategory(category); 766 categories.put(category, ret); 767 changed = true; 768 } 769 } 770 771 return ret; 772 } 773 774 public void removeCategory(ConfigCategory category) 775 { 776 for (ConfigCategory child : category.getChildren()) 777 { 778 removeCategory(child); 779 } 780 781 if (categories.containsKey(category.getQualifiedName())) 782 { 783 categories.remove(category.getQualifiedName()); 784 if (category.parent != null) 785 { 786 category.parent.removeChild(category); 787 } 788 changed = true; 789 } 790 } 791 792 public void addCustomCategoryComment(String category, String comment) 793 { 794 if (!caseSensitiveCustomCategories) 795 category = category.toLowerCase(Locale.ENGLISH); 796 getCategory(category).setComment(comment); 797 } 798 799 private void setChild(String name, Configuration child) 800 { 801 if (!children.containsKey(name)) 802 { 803 children.put(name, child); 804 changed = true; 805 } 806 else 807 { 808 Configuration old = children.get(name); 809 child.categories = old.categories; 810 child.fileName = old.fileName; 811 old.changed = true; 812 } 813 } 814 815 public static void enableGlobalConfig() 816 { 817 PARENT = new Configuration(new File(Loader.instance().getConfigDir(), "global.cfg")); 818 PARENT.load(); 819 } 820 821 public static class UnicodeInputStreamReader extends Reader 822 { 823 private final InputStreamReader input; 824 private final String defaultEnc; 825 826 public UnicodeInputStreamReader(InputStream source, String encoding) throws IOException 827 { 828 defaultEnc = encoding; 829 String enc = encoding; 830 byte[] data = new byte[4]; 831 832 PushbackInputStream pbStream = new PushbackInputStream(source, data.length); 833 int read = pbStream.read(data, 0, data.length); 834 int size = 0; 835 836 int bom16 = (data[0] & 0xFF) << 8 | (data[1] & 0xFF); 837 int bom24 = bom16 << 8 | (data[2] & 0xFF); 838 int bom32 = bom24 << 8 | (data[3] & 0xFF); 839 840 if (bom24 == 0xEFBBBF) 841 { 842 enc = "UTF-8"; 843 size = 3; 844 } 845 else if (bom16 == 0xFEFF) 846 { 847 enc = "UTF-16BE"; 848 size = 2; 849 } 850 else if (bom16 == 0xFFFE) 851 { 852 enc = "UTF-16LE"; 853 size = 2; 854 } 855 else if (bom32 == 0x0000FEFF) 856 { 857 enc = "UTF-32BE"; 858 size = 4; 859 } 860 else if (bom32 == 0xFFFE0000) //This will never happen as it'll be caught by UTF-16LE, 861 { //but if anyone ever runs across a 32LE file, i'd like to disect it. 862 enc = "UTF-32LE"; 863 size = 4; 864 } 865 866 if (size < read) 867 { 868 pbStream.unread(data, size, read - size); 869 } 870 871 this.input = new InputStreamReader(pbStream, enc); 872 } 873 874 public String getEncoding() 875 { 876 return input.getEncoding(); 877 } 878 879 @Override 880 public int read(char[] cbuf, int off, int len) throws IOException 881 { 882 return input.read(cbuf, off, len); 883 } 884 885 @Override 886 public void close() throws IOException 887 { 888 input.close(); 889 } 890 } 891 892 public boolean hasChanged() 893 { 894 if (changed) return true; 895 896 for (ConfigCategory cat : categories.values()) 897 { 898 if (cat.hasChanged()) return true; 899 } 900 901 for (Configuration child : children.values()) 902 { 903 if (child.hasChanged()) return true; 904 } 905 906 return false; 907 } 908 909 private void resetChangedState() 910 { 911 changed = false; 912 for (ConfigCategory cat : categories.values()) 913 { 914 cat.resetChangedState(); 915 } 916 917 for (Configuration child : children.values()) 918 { 919 child.resetChangedState(); 920 } 921 } 922 923 public Set<String> getCategoryNames() 924 { 925 return ImmutableSet.copyOf(categories.keySet()); 926 } 927}