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 addCustomCategoryComment(String category, String comment)
775    {
776        if (!caseSensitiveCustomCategories)
777            category = category.toLowerCase(Locale.ENGLISH);
778        getCategory(category).setComment(comment);
779    }
780
781    private void setChild(String name, Configuration child)
782    {
783        if (!children.containsKey(name))
784        {
785            children.put(name, child);
786            changed = true;
787        }
788        else
789        {
790            Configuration old = children.get(name);
791            child.categories = old.categories;
792            child.fileName = old.fileName;
793            old.changed = true;
794        }
795    }
796
797    public static void enableGlobalConfig()
798    {
799        PARENT = new Configuration(new File(Loader.instance().getConfigDir(), "global.cfg"));
800        PARENT.load();
801    }
802
803    public static class UnicodeInputStreamReader extends Reader
804    {
805        private final InputStreamReader input;
806        private final String defaultEnc;
807
808        public UnicodeInputStreamReader(InputStream source, String encoding) throws IOException
809        {
810            defaultEnc = encoding;
811            String enc = encoding;
812            byte[] data = new byte[4];
813
814            PushbackInputStream pbStream = new PushbackInputStream(source, data.length);
815            int read = pbStream.read(data, 0, data.length);
816            int size = 0;
817
818            int bom16 = (data[0] & 0xFF) << 8 | (data[1] & 0xFF);
819            int bom24 = bom16 << 8 | (data[2] & 0xFF);
820            int bom32 = bom24 << 8 | (data[3] & 0xFF);
821
822            if (bom24 == 0xEFBBBF)
823            {
824                enc = "UTF-8";
825                size = 3;
826            }
827            else if (bom16 == 0xFEFF)
828            {
829                enc = "UTF-16BE";
830                size = 2;
831            }
832            else if (bom16 == 0xFFFE)
833            {
834                enc = "UTF-16LE";
835                size = 2;
836            }
837            else if (bom32 == 0x0000FEFF)
838            {
839                enc = "UTF-32BE";
840                size = 4;
841            }
842            else if (bom32 == 0xFFFE0000) //This will never happen as it'll be caught by UTF-16LE,
843            {                             //but if anyone ever runs across a 32LE file, i'd like to disect it.
844                enc = "UTF-32LE";
845                size = 4;
846            }
847
848            if (size < read)
849            {
850                pbStream.unread(data, size, read - size);
851            }
852
853            this.input = new InputStreamReader(pbStream, enc);
854        }
855
856        public String getEncoding()
857        {
858            return input.getEncoding();
859        }
860
861        @Override
862        public int read(char[] cbuf, int off, int len) throws IOException
863        {
864            return input.read(cbuf, off, len);
865        }
866
867        @Override
868        public void close() throws IOException
869        {
870            input.close();
871        }
872    }
873
874    public boolean hasChanged()
875    {
876        if (changed) return true;
877        
878        for (ConfigCategory cat : categories.values())
879        {
880            if (cat.hasChanged()) return true;
881        }
882
883        for (Configuration child : children.values())
884        {
885            if (child.hasChanged()) return true;
886        }
887
888        return false;
889    }
890
891    private void resetChangedState()
892    {
893        changed = false;
894        for (ConfigCategory cat : categories.values())
895        {
896            cat.resetChangedState();
897        }
898
899        for (Configuration child : children.values())
900        {
901            child.resetChangedState();
902        }
903    }
904
905    public Set<String> getCategoryNames()
906    {
907        return ImmutableSet.copyOf(categories.keySet());
908    }
909}