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