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 + NEW_LINE);
678
679                if (children.isEmpty())
680                {
681                    save(buffer);
682                }
683                else
684                {
685                    for (Map.Entry<String, Configuration> entry : children.entrySet())
686                    {
687                        buffer.write("START: \"" + entry.getKey() + "\"" + NEW_LINE);
688                        entry.getValue().save(buffer);
689                        buffer.write("END: \"" + entry.getKey() + "\"" + NEW_LINE + NEW_LINE);
690                    }
691                }
692
693                buffer.close();
694                fos.close();
695            }
696        }
697        catch (IOException e)
698        {
699            e.printStackTrace();
700        }
701    }
702
703    private void save(BufferedWriter out) throws IOException
704    {
705        //For compatiblitties sake just in case, Thanks Atomic, to be removed next MC version
706        //TO-DO: Remove next MC version
707        Object[] categoryArray = categories.values().toArray();
708        for (Object o : categoryArray)
709        {
710            if (o instanceof TreeMap)
711            {
712                TreeMap treeMap = (TreeMap)o;
713                ConfigCategory converted = new ConfigCategory(file.getName());
714                FMLLog.warning("Forge found a Treemap saved for Configuration file " + file.getName() + ", this is deprecated behaviour!");
715                
716                for (Object key : treeMap.keySet())
717                {
718                    FMLLog.warning("Converting Treemap to ConfigCategory, key: " + key + ", property value: " + ((Property)treeMap.get(key)).value);
719                    converted.set((String)key, (Property)treeMap.get(key));
720                }
721                
722                categories.values().remove(o);
723                categories.put(file.getName(), converted);
724            }
725        }
726        
727        for (ConfigCategory cat : categories.values())
728        {
729            if (!cat.isChild())
730            {
731                cat.write(out, 0);
732                out.newLine();
733            }
734        }
735    }
736
737    public ConfigCategory getCategory(String category)
738    {
739        ConfigCategory ret = categories.get(category);
740
741        if (ret == null)
742        {
743            if (category.contains(CATEGORY_SPLITTER))
744            {
745                String[] hierarchy = category.split("\\"+CATEGORY_SPLITTER);
746                ConfigCategory parent = categories.get(hierarchy[0]);
747
748                if (parent == null)
749                {
750                    parent = new ConfigCategory(hierarchy[0]);
751                    categories.put(parent.getQualifiedName(), parent);
752                }
753
754                for (int i = 1; i < hierarchy.length; i++)
755                {
756                    String name = ConfigCategory.getQualifiedName(hierarchy[i], parent);
757                    ConfigCategory child = categories.get(name);
758
759                    if (child == null)
760                    {
761                        child = new ConfigCategory(hierarchy[i], parent);
762                        categories.put(name, child);
763                    }
764
765                    ret = child;
766                    parent = child;
767                }
768            }
769            else
770            {
771                ret = new ConfigCategory(category);
772                categories.put(category, ret);
773            }
774        }
775
776        return ret;
777    }
778
779    public void addCustomCategoryComment(String category, String comment)
780    {
781        if (!caseSensitiveCustomCategories)
782            category = category.toLowerCase(Locale.ENGLISH);
783        getCategory(category).setComment(comment);
784    }
785
786    private void setChild(String name, Configuration child)
787    {
788        if (!children.containsKey(name))
789        {
790            children.put(name, child);
791        }
792        else
793        {
794            Configuration old = children.get(name);
795            child.categories = old.categories;
796            child.fileName = old.fileName;
797        }
798    }
799
800    public static void enableGlobalConfig()
801    {
802        PARENT = new Configuration(new File(Loader.instance().getConfigDir(), "global.cfg"));
803        PARENT.load();
804    }
805
806    public static class UnicodeInputStreamReader extends Reader
807    {
808        private final InputStreamReader input;
809        private final String defaultEnc;
810
811        public UnicodeInputStreamReader(InputStream source, String encoding) throws IOException
812        {
813            defaultEnc = encoding;
814            String enc = encoding;
815            byte[] data = new byte[4];
816
817            PushbackInputStream pbStream = new PushbackInputStream(source, data.length);
818            int read = pbStream.read(data, 0, data.length);
819            int size = 0;
820
821            int bom16 = (data[0] & 0xFF) << 8 | (data[1] & 0xFF);
822            int bom24 = bom16 << 8 | (data[2] & 0xFF);
823            int bom32 = bom24 << 8 | (data[3] & 0xFF);
824
825            if (bom24 == 0xEFBBBF)
826            {
827                enc = "UTF-8";
828                size = 3;
829            }
830            else if (bom16 == 0xFEFF)
831            {
832                enc = "UTF-16BE";
833                size = 2;
834            }
835            else if (bom16 == 0xFFFE)
836            {
837                enc = "UTF-16LE";
838                size = 2;
839            }
840            else if (bom32 == 0x0000FEFF)
841            {
842                enc = "UTF-32BE";
843                size = 4;
844            }
845            else if (bom32 == 0xFFFE0000) //This will never happen as it'll be caught by UTF-16LE,
846            {                             //but if anyone ever runs across a 32LE file, i'd like to disect it.
847                enc = "UTF-32LE";
848                size = 4;
849            }
850
851            if (size < read)
852            {
853                pbStream.unread(data, size, read - size);
854            }
855
856            this.input = new InputStreamReader(pbStream, enc);
857        }
858
859        public String getEncoding()
860        {
861            return input.getEncoding();
862        }
863
864        @Override
865        public int read(char[] cbuf, int off, int len) throws IOException
866        {
867            return input.read(cbuf, off, len);
868        }
869
870        @Override
871        public void close() throws IOException
872        {
873            input.close();
874        }
875    }
876}