001 /**
002 * This software is provided under the terms of the Minecraft Forge Public
003 * License v1.0.
004 */
005
006 package net.minecraftforge.common;
007
008 import java.io.*;
009 import java.text.DateFormat;
010 import java.util.ArrayList;
011 import java.util.Arrays;
012 import java.util.Collection;
013 import java.util.Date;
014 import java.util.Locale;
015 import java.util.Map;
016 import java.util.TreeMap;
017 import java.util.regex.Matcher;
018 import java.util.regex.Pattern;
019
020 import com.google.common.base.CharMatcher;
021 import com.google.common.base.Splitter;
022 import com.google.common.collect.Maps;
023
024 import cpw.mods.fml.common.FMLCommonHandler;
025 import cpw.mods.fml.common.FMLLog;
026 import cpw.mods.fml.common.Loader;
027 import cpw.mods.fml.relauncher.FMLInjectionData;
028
029 import net.minecraft.block.Block;
030 import net.minecraft.item.Item;
031 import 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 */
037 public 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 try
444 {
445 if (file.getParentFile() != null)
446 {
447 file.getParentFile().mkdirs();
448 }
449
450 if (!file.exists() && !file.createNewFile())
451 {
452 return;
453 }
454
455 if (file.canRead())
456 {
457 UnicodeInputStreamReader input = new UnicodeInputStreamReader(new FileInputStream(file), defaultEncoding);
458 defaultEncoding = input.getEncoding();
459 buffer = new BufferedReader(input);
460
461 String line;
462 ConfigCategory currentCat = null;
463 Property.Type type = null;
464 ArrayList<String> tmpList = null;
465 int lineNum = 0;
466 String name = null;
467
468 while (true)
469 {
470 lineNum++;
471 line = buffer.readLine();
472
473 if (line == null)
474 {
475 break;
476 }
477
478 Matcher start = CONFIG_START.matcher(line);
479 Matcher end = CONFIG_END.matcher(line);
480
481 if (start.matches())
482 {
483 fileName = start.group(1);
484 categories = new TreeMap<String, ConfigCategory>();
485 continue;
486 }
487 else if (end.matches())
488 {
489 fileName = end.group(1);
490 Configuration child = new Configuration();
491 child.categories = categories;
492 this.children.put(fileName, child);
493 continue;
494 }
495
496 int nameStart = -1, nameEnd = -1;
497 boolean skip = false;
498 boolean quoted = false;
499
500 for (int i = 0; i < line.length() && !skip; ++i)
501 {
502 if (Character.isLetterOrDigit(line.charAt(i)) || ALLOWED_CHARS.indexOf(line.charAt(i)) != -1 || (quoted && line.charAt(i) != '"'))
503 {
504 if (nameStart == -1)
505 {
506 nameStart = i;
507 }
508
509 nameEnd = i;
510 }
511 else if (Character.isWhitespace(line.charAt(i)))
512 {
513 // ignore space charaters
514 }
515 else
516 {
517 switch (line.charAt(i))
518 {
519 case '#':
520 skip = true;
521 continue;
522
523 case '"':
524 if (quoted)
525 {
526 quoted = false;
527 }
528 if (!quoted && nameStart == -1)
529 {
530 quoted = true;
531 }
532 break;
533
534 case '{':
535 name = line.substring(nameStart, nameEnd + 1);
536 String qualifiedName = ConfigCategory.getQualifiedName(name, currentCat);
537
538 ConfigCategory cat = categories.get(qualifiedName);
539 if (cat == null)
540 {
541 currentCat = new ConfigCategory(name, currentCat);
542 categories.put(qualifiedName, currentCat);
543 }
544 else
545 {
546 currentCat = cat;
547 }
548 name = null;
549
550 break;
551
552 case '}':
553 if (currentCat == null)
554 {
555 throw new RuntimeException(String.format("Config file corrupt, attepted to close to many categories '%s:%d'", fileName, lineNum));
556 }
557 currentCat = currentCat.parent;
558 break;
559
560 case '=':
561 name = line.substring(nameStart, nameEnd + 1);
562
563 if (currentCat == null)
564 {
565 throw new RuntimeException(String.format("'%s' has no scope in '%s:%d'", name, fileName, lineNum));
566 }
567
568 Property prop = new Property(name, line.substring(i + 1), type, true);
569 i = line.length();
570
571 currentCat.set(name, prop);
572
573 break;
574
575 case ':':
576 type = Property.Type.tryParse(line.substring(nameStart, nameEnd + 1).charAt(0));
577 nameStart = nameEnd = -1;
578 break;
579
580 case '<':
581 if (tmpList != null)
582 {
583 throw new RuntimeException(String.format("Malformed list property \"%s:%d\"", fileName, lineNum));
584 }
585
586 name = line.substring(nameStart, nameEnd + 1);
587
588 if (currentCat == null)
589 {
590 throw new RuntimeException(String.format("'%s' has no scope in '%s:%d'", name, fileName, lineNum));
591 }
592
593 tmpList = new ArrayList<String>();
594
595 skip = true;
596
597 break;
598
599 case '>':
600 if (tmpList == null)
601 {
602 throw new RuntimeException(String.format("Malformed list property \"%s:%d\"", fileName, lineNum));
603 }
604
605 currentCat.set(name, new Property(name, tmpList.toArray(new String[tmpList.size()]), type));
606 name = null;
607 tmpList = null;
608 type = null;
609 break;
610
611 default:
612 throw new RuntimeException(String.format("Unknown character '%s' in '%s:%d'", line.charAt(i), fileName, lineNum));
613 }
614 }
615 }
616
617 if (quoted)
618 {
619 throw new RuntimeException(String.format("Unmatched quote in '%s:%d'", fileName, lineNum));
620 }
621 else if (tmpList != null && !skip)
622 {
623 tmpList.add(line.trim());
624 }
625 }
626 }
627 }
628 catch (IOException e)
629 {
630 e.printStackTrace();
631 }
632 finally
633 {
634 if (buffer != null)
635 {
636 try
637 {
638 buffer.close();
639 } catch (IOException e){}
640 }
641 }
642 }
643
644 public void save()
645 {
646 if (PARENT != null && PARENT != this)
647 {
648 PARENT.save();
649 return;
650 }
651
652 try
653 {
654 if (file.getParentFile() != null)
655 {
656 file.getParentFile().mkdirs();
657 }
658
659 if (!file.exists() && !file.createNewFile())
660 {
661 return;
662 }
663
664 if (file.canWrite())
665 {
666 FileOutputStream fos = new FileOutputStream(file);
667 BufferedWriter buffer = new BufferedWriter(new OutputStreamWriter(fos, defaultEncoding));
668
669 buffer.write("# Configuration file" + NEW_LINE);
670 buffer.write("# Generated on " + DateFormat.getInstance().format(new Date()) + NEW_LINE + NEW_LINE);
671
672 if (children.isEmpty())
673 {
674 save(buffer);
675 }
676 else
677 {
678 for (Map.Entry<String, Configuration> entry : children.entrySet())
679 {
680 buffer.write("START: \"" + entry.getKey() + "\"" + NEW_LINE);
681 entry.getValue().save(buffer);
682 buffer.write("END: \"" + entry.getKey() + "\"" + NEW_LINE + NEW_LINE);
683 }
684 }
685
686 buffer.close();
687 fos.close();
688 }
689 }
690 catch (IOException e)
691 {
692 e.printStackTrace();
693 }
694 }
695
696 private void save(BufferedWriter out) throws IOException
697 {
698 //For compatiblitties sake just in case, Thanks Atomic, to be removed next MC version
699 //TO-DO: Remove next MC version
700 Object[] categoryArray = categories.values().toArray();
701 for (Object o : categoryArray)
702 {
703 if (o instanceof TreeMap)
704 {
705 TreeMap treeMap = (TreeMap)o;
706 ConfigCategory converted = new ConfigCategory(file.getName());
707 FMLLog.warning("Forge found a Treemap saved for Configuration file " + file.getName() + ", this is deprecated behaviour!");
708
709 for (Object key : treeMap.keySet())
710 {
711 FMLLog.warning("Converting Treemap to ConfigCategory, key: " + key + ", property value: " + ((Property)treeMap.get(key)).value);
712 converted.set((String)key, (Property)treeMap.get(key));
713 }
714
715 categories.values().remove(o);
716 categories.put(file.getName(), converted);
717 }
718 }
719
720 for (ConfigCategory cat : categories.values())
721 {
722 if (!cat.isChild())
723 {
724 cat.write(out, 0);
725 out.newLine();
726 }
727 }
728 }
729
730 public ConfigCategory getCategory(String category)
731 {
732 ConfigCategory ret = categories.get(category);
733
734 if (ret == null)
735 {
736 if (category.contains(CATEGORY_SPLITTER))
737 {
738 String[] hierarchy = category.split("\\"+CATEGORY_SPLITTER);
739 ConfigCategory parent = categories.get(hierarchy[0]);
740
741 if (parent == null)
742 {
743 parent = new ConfigCategory(hierarchy[0]);
744 categories.put(parent.getQualifiedName(), parent);
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 }
757
758 ret = child;
759 parent = child;
760 }
761 }
762 else
763 {
764 ret = new ConfigCategory(category);
765 categories.put(category, ret);
766 }
767 }
768
769 return ret;
770 }
771
772 public void addCustomCategoryComment(String category, String comment)
773 {
774 if (!caseSensitiveCustomCategories)
775 category = category.toLowerCase(Locale.ENGLISH);
776 getCategory(category).setComment(comment);
777 }
778
779 private void setChild(String name, Configuration child)
780 {
781 if (!children.containsKey(name))
782 {
783 children.put(name, child);
784 }
785 else
786 {
787 Configuration old = children.get(name);
788 child.categories = old.categories;
789 child.fileName = old.fileName;
790 }
791 }
792
793 public static void enableGlobalConfig()
794 {
795 PARENT = new Configuration(new File(Loader.instance().getConfigDir(), "global.cfg"));
796 PARENT.load();
797 }
798
799 public static class UnicodeInputStreamReader extends Reader
800 {
801 private final InputStreamReader input;
802 private final String defaultEnc;
803
804 public UnicodeInputStreamReader(InputStream source, String encoding) throws IOException
805 {
806 defaultEnc = encoding;
807 String enc = encoding;
808 byte[] data = new byte[4];
809
810 PushbackInputStream pbStream = new PushbackInputStream(source, data.length);
811 int read = pbStream.read(data, 0, data.length);
812 int size = 0;
813
814 int bom16 = (data[0] & 0xFF) << 8 | (data[1] & 0xFF);
815 int bom24 = bom16 << 8 | (data[2] & 0xFF);
816 int bom32 = bom24 << 8 | (data[3] & 0xFF);
817
818 if (bom24 == 0xEFBBBF)
819 {
820 enc = "UTF-8";
821 size = 3;
822 }
823 else if (bom16 == 0xFEFF)
824 {
825 enc = "UTF-16BE";
826 size = 2;
827 }
828 else if (bom16 == 0xFFFE)
829 {
830 enc = "UTF-16LE";
831 size = 2;
832 }
833 else if (bom32 == 0x0000FEFF)
834 {
835 enc = "UTF-32BE";
836 size = 4;
837 }
838 else if (bom32 == 0xFFFE0000) //This will never happen as it'll be caught by UTF-16LE,
839 { //but if anyone ever runs across a 32LE file, i'd like to disect it.
840 enc = "UTF-32LE";
841 size = 4;
842 }
843
844 if (size < read)
845 {
846 pbStream.unread(data, size, read - size);
847 }
848
849 this.input = new InputStreamReader(pbStream, enc);
850 }
851
852 public String getEncoding()
853 {
854 return input.getEncoding();
855 }
856
857 @Override
858 public int read(char[] cbuf, int off, int len) throws IOException
859 {
860 return input.read(cbuf, off, len);
861 }
862
863 @Override
864 public void close() throws IOException
865 {
866 input.close();
867 }
868 }
869 }