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.src.Block;
030    import net.minecraft.src.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         * Normal mods such as '50 new ores' do not need to be below 256 so should use the normal getBlock
117         */
118        public Property getTerrainBlock(String category, String key, int defaultID, String comment)
119        {
120            return getBlockInternal(category, key, defaultID, comment, 0, 256); 
121        }
122    
123        private Property getBlockInternal(String category, String key, int defaultID, String comment, int lower, int upper)
124        {
125            Property prop = get(category, key, -1, comment);
126    
127            if (prop.getInt() != -1)
128            {
129                configMarkers[prop.getInt()] = true;
130                return prop;
131            }
132            else
133            {
134                if (defaultID < lower)
135                {
136                    FMLLog.warning(
137                        "Mod attempted to get a block ID with a default in the Terrain Generation section, " +
138                        "mod authors should make sure there defaults are above 256 unless explicitly needed " +
139                        "for terrain generation. Most ores do not need to be below 256.");
140                    FMLLog.warning("Config \"%s\" Category: \"%s\" Key: \"%s\" Default: %d", fileName, category, key, defaultID);
141                    defaultID = upper - 1;
142                }
143    
144                if (Block.blocksList[defaultID] == null && !configMarkers[defaultID])
145                {
146                    prop.value = Integer.toString(defaultID);
147                    configMarkers[defaultID] = true;
148                    return prop;
149                }
150                else
151                {
152                    for (int j = upper - 1; j > 0; j--)
153                    {
154                        if (Block.blocksList[j] == null && !configMarkers[j])
155                        {
156                            prop.value = Integer.toString(j);
157                            configMarkers[j] = true;
158                            return prop;
159                        }
160                    }
161    
162                    throw new RuntimeException("No more block ids available for " + key);
163                }
164            }
165        }
166    
167        public Property getItem(String key, int defaultID) { return getItem(CATEGORY_ITEM, key, defaultID, null); }
168        public Property getItem(String key, int defaultID, String comment) { return getItem(CATEGORY_ITEM, key, defaultID, comment); }
169        public Property getItem(String category, String key, int defaultID) { return getItem(category, key, defaultID, null); }
170    
171        public Property getItem(String category, String key, int defaultID, String comment)
172        {
173            Property prop = get(category, key, -1, comment);
174            int defaultShift = defaultID + ITEM_SHIFT;
175    
176            if (prop.getInt() != -1)
177            {
178                configMarkers[prop.getInt() + ITEM_SHIFT] = true;
179                return prop;
180            }
181            else
182            {
183                if (defaultID < MAX_BLOCKS - ITEM_SHIFT)
184                {
185                    FMLLog.warning(
186                        "Mod attempted to get a item ID with a default value in the block ID section, " +
187                        "mod authors should make sure there defaults are above %d unless explicitly needed " +
188                        "so that all block ids are free to store blocks.", MAX_BLOCKS - ITEM_SHIFT);
189                    FMLLog.warning("Config \"%s\" Category: \"%s\" Key: \"%s\" Default: %d", fileName, category, key, defaultID);
190                }
191    
192                if (Item.itemsList[defaultShift] == null && !configMarkers[defaultShift] && defaultShift > Block.blocksList.length)
193                {
194                    prop.value = Integer.toString(defaultID);
195                    configMarkers[defaultShift] = true;
196                    return prop;
197                }
198                else
199                {
200                    for (int x = Item.itemsList.length - 1; x >= ITEM_SHIFT; x--)
201                    {
202                        if (Item.itemsList[x] == null && !configMarkers[x])
203                        {
204                            prop.value = Integer.toString(x - ITEM_SHIFT);
205                            configMarkers[x] = true;
206                            return prop;
207                        }
208                    }
209    
210                    throw new RuntimeException("No more item ids available for " + key);
211                }
212            }
213        }
214    
215        public Property get(String category, String key, int defaultValue)
216        {
217            return get(category, key, defaultValue, null);
218        }
219    
220        public Property get(String category, String key, int defaultValue, String comment)
221        {
222            Property prop = get(category, key, Integer.toString(defaultValue), comment, INTEGER);
223            if (!prop.isIntValue())
224            {
225                prop.value = Integer.toString(defaultValue);
226            }
227            return prop;
228        }
229    
230        public Property get(String category, String key, boolean defaultValue)
231        {
232            return get(category, key, defaultValue, null);
233        }
234    
235        public Property get(String category, String key, boolean defaultValue, String comment)
236        {
237            Property prop = get(category, key, Boolean.toString(defaultValue), comment, BOOLEAN);
238            if (!prop.isBooleanValue())
239            {
240                prop.value = Boolean.toString(defaultValue);
241            }
242            return prop;
243        }
244    
245        public Property get(String category, String key, double defaultValue)
246        {
247            return get(category, key, defaultValue, null);
248        }
249    
250        public Property get(String category, String key, double defaultValue, String comment)
251        {
252            Property prop = get(category, key, Double.toString(defaultValue), comment, DOUBLE);
253            if (!prop.isDoubleValue())
254            {
255                prop.value = Double.toString(defaultValue);
256            }
257            return prop;
258        }
259    
260        public Property get(String category, String key, String defaultValue)
261        {
262            return get(category, key, defaultValue, null);
263        }
264    
265        public Property get(String category, String key, String defaultValue, String comment)
266        {
267            return get(category, key, defaultValue, comment, STRING);
268        }
269    
270        public Property get(String category, String key, String[] defaultValue)
271        {
272            return get(category, key, defaultValue, null);
273        }
274    
275        public Property get(String category, String key, String[] defaultValue, String comment)
276        {
277            return get(category, key, defaultValue, comment, STRING);
278        }
279    
280        public Property get(String category, String key, int[] defaultValue)
281        {
282            return get(category, key, defaultValue, null);
283        }
284    
285        public Property get(String category, String key, int[] defaultValue, String comment)
286        {
287            String[] values = new String[defaultValue.length];
288            for (int i = 0; i < defaultValue.length; i++)
289            {
290                values[i] = Integer.toString(defaultValue[i]);
291            }
292    
293            Property prop =  get(category, key, values, comment, INTEGER);
294            if (!prop.isIntList())
295            {
296                prop.valueList = values;
297            }
298    
299            return prop;
300        }
301    
302        public Property get(String category, String key, double[] defaultValue)
303        {
304            return get(category, key, defaultValue, null);
305        }
306    
307        public Property get(String category, String key, double[] defaultValue, String comment)
308        {
309            String[] values = new String[defaultValue.length];
310            for (int i = 0; i < defaultValue.length; i++)
311            {
312                values[i] = Double.toString(defaultValue[i]);
313            }
314    
315            Property prop =  get(category, key, values, comment, DOUBLE);
316            
317            if (!prop.isDoubleList())
318            {
319                prop.valueList = values;
320            }
321    
322            return prop;
323        }
324    
325        public Property get(String category, String key, boolean[] defaultValue)
326        {
327            return get(category, key, defaultValue, null);
328        }
329    
330        public Property get(String category, String key, boolean[] defaultValue, String comment)
331        {
332            String[] values = new String[defaultValue.length];
333            for (int i = 0; i < defaultValue.length; i++)
334            {
335                values[i] = Boolean.toString(defaultValue[i]);
336            }
337    
338            Property prop =  get(category, key, values, comment, BOOLEAN);
339            
340            if (!prop.isBooleanList())
341            {
342                prop.valueList = values;
343            }
344    
345            return prop;
346        }
347    
348        public Property get(String category, String key, String defaultValue, String comment, Property.Type type)
349        {
350            if (!caseSensitiveCustomCategories)
351            {
352                category = category.toLowerCase(Locale.ENGLISH);
353            }
354    
355            ConfigCategory cat = getCategory(category);
356    
357            if (cat.containsKey(key))
358            {
359                Property prop = cat.get(key);
360    
361                if (prop.getType() == null)
362                {
363                    prop = new Property(prop.getName(), prop.value, type);
364                    cat.set(key, prop);
365                }
366    
367                prop.comment = comment;
368                return prop;
369            }
370            else if (defaultValue != null)
371            {
372                Property prop = new Property(key, defaultValue, type);
373                cat.set(key, prop);
374                prop.comment = comment;
375                return prop;
376            }
377            else
378            {
379                return null;
380            }
381        }
382    
383        public Property get(String category, String key, String[] defaultValue, String comment, Property.Type type)
384        {
385            if (!caseSensitiveCustomCategories)
386            {
387                category = category.toLowerCase(Locale.ENGLISH);
388            }
389    
390            ConfigCategory cat = getCategory(category);
391    
392            if (cat.containsKey(key))
393            {
394                Property prop = cat.get(key);
395    
396                if (prop.getType() == null)
397                {
398                    prop = new Property(prop.getName(), prop.value, type);
399                    cat.set(key, prop);
400                }
401    
402                prop.comment = comment;
403    
404                return prop;
405            }
406            else if (defaultValue != null)
407            {
408                Property prop = new Property(key, defaultValue, type);
409                prop.comment = comment;
410                cat.set(key, prop);
411                return prop;
412            }
413            else
414            {
415                return null;
416            }
417        }
418    
419        public boolean hasCategory(String category)
420        {
421            return categories.get(category) != null;
422        }
423    
424        public boolean hasKey(String category, String key)
425        {
426            ConfigCategory cat = categories.get(category);
427            return cat != null && cat.containsKey(key);
428        }
429    
430        public void load()
431        {
432            if (PARENT != null && PARENT != this)
433            {
434                return;
435            }
436    
437            BufferedReader buffer = null;
438            try
439            {
440                if (file.getParentFile() != null)
441                {
442                    file.getParentFile().mkdirs();
443                }
444    
445                if (!file.exists() && !file.createNewFile())
446                {
447                    return;
448                }
449    
450                if (file.canRead())
451                {
452                    UnicodeInputStreamReader input = new UnicodeInputStreamReader(new FileInputStream(file), defaultEncoding);
453                    defaultEncoding = input.getEncoding();
454                    buffer = new BufferedReader(input);
455    
456                    String line;
457                    ConfigCategory currentCat = null;
458                    Property.Type type = null;
459                    ArrayList<String> tmpList = null;
460                    int lineNum = 0;
461                    String name = null;
462    
463                    while (true)
464                    {
465                        lineNum++;
466                        line = buffer.readLine();
467    
468                        if (line == null)
469                        {
470                            break;
471                        }
472    
473                        Matcher start = CONFIG_START.matcher(line);
474                        Matcher end = CONFIG_END.matcher(line);
475    
476                        if (start.matches())
477                        {
478                            fileName = start.group(1);
479                            categories = new TreeMap<String, ConfigCategory>();
480                            continue;
481                        }
482                        else if (end.matches())
483                        {
484                            fileName = end.group(1);
485                            Configuration child = new Configuration();
486                            child.categories = categories;
487                            this.children.put(fileName, child);
488                            continue;
489                        }
490    
491                        int nameStart = -1, nameEnd = -1;
492                        boolean skip = false;
493                        boolean quoted = false;
494    
495                        for (int i = 0; i < line.length() && !skip; ++i)
496                        {
497                            if (Character.isLetterOrDigit(line.charAt(i)) || ALLOWED_CHARS.indexOf(line.charAt(i)) != -1 || (quoted && line.charAt(i) != '"'))
498                            {
499                                if (nameStart == -1)
500                                {
501                                    nameStart = i;
502                                }
503    
504                                nameEnd = i;
505                            }
506                            else if (Character.isWhitespace(line.charAt(i)))
507                            {
508                                // ignore space charaters
509                            }
510                            else
511                            {
512                                switch (line.charAt(i))
513                                {
514                                    case '#':
515                                        skip = true;
516                                        continue;
517    
518                                    case '"':
519                                        if (quoted)
520                                        {
521                                            quoted = false;
522                                        }
523                                        if (!quoted && nameStart == -1)
524                                        {
525                                            quoted = true;
526                                        }
527                                        break;
528    
529                                    case '{':
530                                        name = line.substring(nameStart, nameEnd + 1);
531                                        String qualifiedName = ConfigCategory.getQualifiedName(name, currentCat);
532    
533                                        currentCat = categories.get(qualifiedName);
534                                        if (currentCat == null)
535                                        {
536                                            currentCat = new ConfigCategory(name, null);
537                                            categories.put(qualifiedName, currentCat);
538                                        }
539                                        name = null;
540    
541                                        break;
542    
543                                    case '}':
544                                        currentCat = currentCat.parent;
545                                        break;
546    
547                                    case '=':
548                                        name = line.substring(nameStart, nameEnd + 1);
549    
550                                        if (currentCat == null)
551                                        {
552                                            throw new RuntimeException(String.format("'%s' has no scope in '%s:%d'", name, fileName, lineNum));
553                                        }
554    
555                                        Property prop = new Property(name, line.substring(i + 1), type, true);
556                                        i = line.length();
557    
558                                        currentCat.set(name, prop);
559    
560                                        break;
561    
562                                    case ':':
563                                        type = Property.Type.tryParse(line.substring(nameStart, nameEnd + 1).charAt(0));
564                                        nameStart = nameEnd = -1;
565                                        break;
566    
567                                    case '<':
568                                        if (tmpList != null)
569                                        {
570                                            throw new RuntimeException(String.format("Malformed list property \"%s:%d\"", fileName, lineNum));
571                                        }
572    
573                                        name = line.substring(nameStart, nameEnd + 1);
574    
575                                        if (currentCat == null)
576                                        {
577                                            throw new RuntimeException(String.format("'%s' has no scope in '%s:%d'", name, fileName, lineNum));
578                                        }
579    
580                                        tmpList = new ArrayList<String>();
581    
582                                        break;
583    
584                                    case '>':
585                                        if (tmpList == null)
586                                        {
587                                            throw new RuntimeException(String.format("Malformed list property \"%s:%d\"", fileName, lineNum));
588                                        }
589    
590                                        currentCat.set(name, new Property(name, tmpList.toArray(new String[tmpList.size()]), type));
591                                        name = null;
592                                        tmpList = null;
593                                        type = null;
594                                        break;
595    
596                                    default:
597                                        throw new RuntimeException(String.format("Unknown character '%s' in '%s:%d'", line.charAt(i), fileName, lineNum));
598                                }
599                            }
600                        }
601    
602                        if (quoted)
603                        {
604                            throw new RuntimeException(String.format("Unmatched quote in '%s:%d'", fileName, lineNum));
605                        }
606                        else if (tmpList != null)
607                        {
608                            tmpList.add(line.trim());
609                        }
610                    }
611                }
612            }
613            catch (IOException e)
614            {
615                e.printStackTrace();
616            }
617            finally
618            {
619                if (buffer != null)
620                {
621                    try
622                    {
623                        buffer.close();
624                    } catch (IOException e){}
625                }
626            }
627        }
628    
629        public void save()
630        {
631            if (PARENT != null && PARENT != this)
632            {
633                PARENT.save();
634                return;
635            }
636    
637            try
638            {
639                if (file.getParentFile() != null)
640                {
641                    file.getParentFile().mkdirs();
642                }
643    
644                if (!file.exists() && !file.createNewFile())
645                {
646                    return;
647                }
648    
649                if (file.canWrite())
650                {
651                    FileOutputStream fos = new FileOutputStream(file);
652                    BufferedWriter buffer = new BufferedWriter(new OutputStreamWriter(fos, defaultEncoding));
653    
654                    buffer.write("# Configuration file" + NEW_LINE);
655                    buffer.write("# Generated on " + DateFormat.getInstance().format(new Date()) + NEW_LINE + NEW_LINE);
656    
657                    if (children.isEmpty())
658                    {
659                        save(buffer);
660                    }
661                    else
662                    {
663                        for (Map.Entry<String, Configuration> entry : children.entrySet())
664                        {
665                            buffer.write("START: \"" + entry.getKey() + "\"" + NEW_LINE);
666                            entry.getValue().save(buffer);
667                            buffer.write("END: \"" + entry.getKey() + "\"" + NEW_LINE + NEW_LINE);
668                        }
669                    }
670    
671                    buffer.close();
672                    fos.close();
673                }
674            }
675            catch (IOException e)
676            {
677                e.printStackTrace();
678            }
679        }
680    
681        private void save(BufferedWriter out) throws IOException
682        {
683            for (ConfigCategory cat : categories.values())
684            {
685                if (!cat.isChild())
686                {
687                    cat.write(out, 0);
688                    out.newLine();
689                }
690            }
691        }
692    
693        public ConfigCategory getCategory(String category)
694        {
695            ConfigCategory ret = categories.get(category);
696    
697            if (ret == null)
698            {
699                if (category.contains(CATEGORY_SPLITTER))
700                {
701                    String[] hierarchy = category.split("\\"+CATEGORY_SPLITTER);
702                    ConfigCategory parent = categories.get(hierarchy[0]);
703    
704                    if (parent == null)
705                    {
706                        parent = new ConfigCategory(hierarchy[0]);
707                        categories.put(parent.getQualifiedName(), parent);
708                    }
709    
710                    for (int i = 1; i < hierarchy.length; i++)
711                    {
712                        String name = ConfigCategory.getQualifiedName(hierarchy[i], parent);
713                        ConfigCategory child = categories.get(name);
714    
715                        if (child == null)
716                        {
717                            child = new ConfigCategory(hierarchy[i], parent);
718                            categories.put(name, child);
719                        }
720    
721                        ret = child;
722                        parent = child;
723                    }
724                }
725                else
726                {
727                    ret = new ConfigCategory(category);
728                    categories.put(category, ret);
729                }
730            }
731    
732            return ret;
733        }
734    
735        public void addCustomCategoryComment(String category, String comment)
736        {
737            if (!caseSensitiveCustomCategories)
738                category = category.toLowerCase(Locale.ENGLISH);
739            getCategory(category).setComment(comment);
740        }
741    
742        private void setChild(String name, Configuration child)
743        {
744            if (!children.containsKey(name))
745            {
746                children.put(name, child);
747            }
748            else
749            {
750                Configuration old = children.get(name);
751                child.categories = old.categories;
752                child.fileName = old.fileName;
753            }
754        }
755    
756        public static void enableGlobalConfig()
757        {
758            PARENT = new Configuration(new File(Loader.instance().getConfigDir(), "global.cfg"));
759            PARENT.load();
760        }
761    
762        public static class UnicodeInputStreamReader extends Reader
763        {
764            private final InputStreamReader input;
765            private final String defaultEnc;
766    
767            public UnicodeInputStreamReader(InputStream source, String encoding) throws IOException
768            {
769                defaultEnc = encoding;
770                String enc = encoding;
771                byte[] data = new byte[4];
772    
773                PushbackInputStream pbStream = new PushbackInputStream(source, data.length);
774                int read = pbStream.read(data, 0, data.length);
775                int size = 0;
776    
777                int bom16 = (data[0] & 0xFF) << 8 | (data[1] & 0xFF);
778                int bom24 = bom16 << 8 | (data[2] & 0xFF);
779                int bom32 = bom24 << 8 | (data[3] & 0xFF);
780    
781                if (bom24 == 0xEFBBBF)
782                {
783                    enc = "UTF-8";
784                    size = 3;
785                }
786                else if (bom16 == 0xFEFF)
787                {
788                    enc = "UTF-16BE";
789                    size = 2;
790                }
791                else if (bom16 == 0xFFFE)
792                {
793                    enc = "UTF-16LE";
794                    size = 2;
795                }
796                else if (bom32 == 0x0000FEFF)
797                {
798                    enc = "UTF-32BE";
799                    size = 4;
800                }
801                else if (bom32 == 0xFFFE0000) //This will never happen as it'll be caught by UTF-16LE,
802                {                             //but if anyone ever runs across a 32LE file, i'd like to disect it.
803                    enc = "UTF-32LE";
804                    size = 4;
805                }
806    
807                if (size < read)
808                {
809                    pbStream.unread(data, size, read - size);
810                }
811    
812                this.input = new InputStreamReader(pbStream, enc);
813            }
814    
815            public String getEncoding()
816            {
817                return input.getEncoding();
818            }
819    
820            @Override
821            public int read(char[] cbuf, int off, int len) throws IOException
822            {
823                return input.read(cbuf, off, len);
824            }
825    
826            @Override
827            public void close() throws IOException
828            {
829                input.close();
830            }
831        }
832    }