    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.Arrays;
    011    import java.util.Collection;
    012    import java.util.Date;
    013    import java.util.Locale;
    014    import java.util.Map;
    015    import java.util.TreeMap;
    016    import java.util.regex.Matcher;
    017    import java.util.regex.Pattern;
    018
    019    import com.google.common.base.CharMatcher;
    020    import com.google.common.base.Splitter;
    021    import com.google.common.collect.Maps;
    022
    023    import cpw.mods.fml.common.FMLCommonHandler;
    024    import cpw.mods.fml.common.Loader;
    025    import cpw.mods.fml.relauncher.FMLInjectionData;
    026
    027    import net.minecraft.src.Block;
    028    import net.minecraft.src.Item;
    029    import static net.minecraftforge.common.Property.Type.*;
    030
    031    /**
    032     * This class offers advanced configurations capabilities, allowing to provide
    033     * various categories for configuration variables.
    034     */
    035    public class Configuration
    036    {
    037        private static boolean[] configBlocks = new boolean[Block.blocksList.length];
    038        private static boolean[] configItems  = new boolean[Item.itemsList.length];
    039        private static final int ITEM_SHIFT = 256;
    040
    041        public static final String CATEGORY_GENERAL = "general";
    042        public static final String CATEGORY_BLOCK   = "block";
    043        public static final String CATEGORY_ITEM    = "item";
    044        public static final String ALLOWED_CHARS = "._-";
    045        public static final String DEFAULT_ENCODING = "UTF-8";
    046        private static final Pattern CONFIG_START = Pattern.compile("START: \"([^\\\"]+)\"");
    047        private static final Pattern CONFIG_END = Pattern.compile("END: \"([^\\\"]+)\"");
    048        private static final CharMatcher allowedProperties = CharMatcher.JAVA_LETTER_OR_DIGIT.or(CharMatcher.anyOf(ALLOWED_CHARS));
    049        private static Configuration PARENT = null;
    050
    051        File file;
    052
    053        public Map<String, Map<String, Property>> categories = new TreeMap<String, Map<String, Property>>();
    054        private Map<String, Configuration> children = new TreeMap<String, Configuration>();
    055
    056        private Map<String,String> customCategoryComments = Maps.newHashMap();
    057        private boolean caseSensitiveCustomCategories;
    058        public String defaultEncoding = DEFAULT_ENCODING;
    059        private String fileName = null;
    060        public boolean isChild = false;
    061
    062        static
    063        {
    064            Arrays.fill(configBlocks, false);
    065            Arrays.fill(configItems,  false);
    066        }
    067
    068        public Configuration(){}
    069
    070        /**
    071         * Create a configuration file for the file given in parameter.
    072         */
    073        public Configuration(File file)
    074        {
    075            this.file = file;
    076            String basePath = ((File)(FMLInjectionData.data()[6])).getAbsolutePath().replace(File.separatorChar, '/').replace("/.", "");
    077            String path = file.getAbsolutePath().replace(File.separatorChar, '/').replace("/./", "/").replace(basePath, "");
    078            if (PARENT != null)
    079            {
    080                PARENT.setChild(path, this);
    081                isChild = true;
    082            }
    083            else
    084            {
    085                load();
    086            }
    087        }
    088
    089        public Configuration(File file, boolean caseSensitiveCustomCategories)
    090        {
    091            this(file);
    092            this.caseSensitiveCustomCategories = caseSensitiveCustomCategories;
    093        }
    094
    095        /**
    096         * Gets or create a block id property. If the block id property key is
    097         * already in the configuration, then it will be used. Otherwise,
    098         * defaultId will be used, except if already taken, in which case this
    099         * will try to determine a free default id.
    100         */
    101        public Property getBlock(String key, int defaultID)
    102        {
    103            return getBlock(CATEGORY_BLOCK, key, defaultID);
    104        }
    105
    106        public Property getBlock(String category, String key, int defaultID)
    107        {
    108            Property prop = get(category, key, -1);
    109
    110            if (prop.getInt() != -1)
    111            {
    112                configBlocks[prop.getInt()] = true;
    113                return prop;
    114            }
    115            else
    116            {
    117                if (Block.blocksList[defaultID] == null && !configBlocks[defaultID])
    118                {
    119                    prop.value = Integer.toString(defaultID);
    120                    configBlocks[defaultID] = true;
    121                    return prop;
    122                }
    123                else
    124                {
    125                    for (int j = configBlocks.length - 1; j > 0; j--)
    126                    {
    127                        if (Block.blocksList[j] == null && !configBlocks[j])
    128                        {
    129                            prop.value = Integer.toString(j);
    130                            configBlocks[j] = true;
    131                            return prop;
    132                        }
    133                    }
    134
    135                    throw new RuntimeException("No more block ids available for " + key);
    136                }
    137            }
    138        }
    139
    140        public Property getItem(String key, int defaultID)
    141        {
    142            return getItem(CATEGORY_ITEM, key, defaultID);
    143        }
    144
    145        public Property getItem(String category, String key, int defaultID)
    146        {
    147            Property prop = get(category, key, -1);
    148            int defaultShift = defaultID + ITEM_SHIFT;
    149
    150            if (prop.getInt() != -1)
    151            {
    152                configItems[prop.getInt() + ITEM_SHIFT] = true;
    153                return prop;
    154            }
    155            else
    156            {
    157                if (Item.itemsList[defaultShift] == null && !configItems[defaultShift] && defaultShift > Block.blocksList.length)
    158                {
    159                    prop.value = Integer.toString(defaultID);
    160                    configItems[defaultShift] = true;
    161                    return prop;
    162                }
    163                else
    164                {
    165                    for (int x = configItems.length - 1; x >= ITEM_SHIFT; x--)
    166                    {
    167                        if (Item.itemsList[x] == null && !configItems[x])
    168                        {
    169                            prop.value = Integer.toString(x - ITEM_SHIFT);
    170                            configItems[x] = true;
    171                            return prop;
    172                        }
    173                    }
    174
    175                    throw new RuntimeException("No more item ids available for " + key);
    176                }
    177            }
    178        }
    179
    180        public Property get(String category, String key, int defaultValue)
    181        {
    182            Property prop = get(category, key, Integer.toString(defaultValue), INTEGER);
    183            if (!prop.isIntValue())
    184            {
    185                prop.value = Integer.toString(defaultValue);
    186            }
    187            return prop;
    188        }
    189
    190        public Property get(String category, String key, boolean defaultValue)
    191        {
    192            Property prop = get(category, key, Boolean.toString(defaultValue), BOOLEAN);
    193            if (!prop.isBooleanValue())
    194            {
    195                prop.value = Boolean.toString(defaultValue);
    196            }
    197            return prop;
    198        }
    199
    200        public Property get(String category, String key, String defaultValue)
    201        {
    202            return get(category, key, defaultValue, STRING);
    203        }
    204
    205        public Property get(String category, String key, String defaultValue, Property.Type type)
    206        {
    207            if (!caseSensitiveCustomCategories)
    208            {
    209                category = category.toLowerCase(Locale.ENGLISH);
    210            }
    211
    212            Map<String, Property> source = categories.get(category);
    213
    214            if(source == null)
    215            {
    216                source = new TreeMap<String, Property>();
    217                categories.put(category, source);
    218            }
    219
    220            if (source.containsKey(key))
    221            {
    222                return source.get(key);
    223            }
    224            else if (defaultValue != null)
    225            {
    226                Property prop = new Property(key, defaultValue, type);
    227                source.put(key, prop);
    228                return prop;
    229            }
    230            else
    231            {
    232                return null;
    233            }
    234        }
    235
    236        public boolean hasCategory(String category)
    237        {
    238            return categories.get(category) != null;
    239        }
    240
    241        public boolean hasKey(String category, String key)
    242        {
    243            Map<String, Property> cat = categories.get(category);
    244            return cat != null && cat.get(key) != null;
    245        }
    246
    247        public void load()
    248        {
    249            if (PARENT != null && PARENT != this)
    250            {
    251                return;
    252            }
    253            BufferedReader buffer = null;
    254            try
    255            {
    256                if (file.getParentFile() != null)
    257                {
    258                    file.getParentFile().mkdirs();
    259                }
    260
    261                if (!file.exists() && !file.createNewFile())
    262                {
    263                    return;
    264                }
    265
    266                if (file.canRead())
    267                {
    268                    UnicodeInputStreamReader input = new UnicodeInputStreamReader(new FileInputStream(file), defaultEncoding);
    269                    defaultEncoding = input.getEncoding();
    270                    buffer = new BufferedReader(input);
    271
    272                    String line;
    273                    Map<String, Property> currentMap = null;
    274
    275                    while (true)
    276                    {
    277                        line = buffer.readLine();
    278
    279                        if (line == null)
    280                        {
    281                            break;
    282                        }
    283
    284                        Matcher start = CONFIG_START.matcher(line);
    285                        Matcher end = CONFIG_END.matcher(line);
    286
    287                        if (start.matches())
    288                        {
    289                            fileName = start.group(1);
    290                            categories = new TreeMap<String, Map<String, Property>>();
    291                            customCategoryComments = Maps.newHashMap();
    292                            continue;
    293                        }
    294                        else if (end.matches())
    295                        {
    296                            fileName = end.group(1);
    297                            Configuration child = new Configuration();
    298                            child.categories = categories;
    299                            child.customCategoryComments = customCategoryComments;
    300                            this.children.put(fileName, child);
    301                            continue;
    302                        }
    303
    304                        int nameStart = -1, nameEnd = -1;
    305                        boolean skip = false;
    306                        boolean quoted = false;
    307                        for (int i = 0; i < line.length() && !skip; ++i)
    308                        {
    309                            if (Character.isLetterOrDigit(line.charAt(i)) || ALLOWED_CHARS.indexOf(line.charAt(i)) != -1 || (quoted && line.charAt(i) != '"'))
    310                            {
    311                                if (nameStart == -1)
    312                                {
    313                                    nameStart = i;
    314                                }
    315
    316                                nameEnd = i;
    317                            }
    318                            else if (Character.isWhitespace(line.charAt(i)))
    319                            {
    320                                // ignore space charaters
    321                            }
    322                            else
    323                            {
    324                                switch (line.charAt(i))
    325                                {
    326                                    case '#':
    327                                        skip = true;
    328                                        continue;
    329
    330                                    case '"':
    331                                        if (quoted)
    332                                        {
    333                                            quoted = false;
    334                                        }
    335                                        if (!quoted && nameStart == -1)
    336                                        {
    337                                            quoted = true;
    338                                        }
    339                                        break;
    340
    341                                    case '{':
    342                                        String scopeName = line.substring(nameStart, nameEnd + 1);
    343
    344                                        currentMap = categories.get(scopeName);
    345                                        if (currentMap == null)
    346                                        {
    347                                            currentMap = new TreeMap<String, Property>();
    348                                            categories.put(scopeName, currentMap);
    349                                        }
    350
    351                                        break;
    352
    353                                    case '}':
    354                                        currentMap = null;
    355                                        break;
    356
    357                                    case '=':
    358                                        String propertyName = line.substring(nameStart, nameEnd + 1);
    359
    360                                        if (currentMap == null)
    361                                        {
    362                                            throw new RuntimeException("property " + propertyName + " has no scope");
    363                                        }
    364
    365                                        Property prop = new Property();
    366                                        prop.setName(propertyName);
    367                                        prop.value = line.substring(i + 1);
    368                                        i = line.length();
    369
    370                                        currentMap.put(propertyName, prop);
    371
    372                                        break;
    373
    374                                    default:
    375                                        throw new RuntimeException("unknown character " + line.charAt(i));
    376                                }
    377                            }
    378                        }
    379                        if (quoted)
    380                        {
    381                            throw new RuntimeException("unmatched quote");
    382                        }
    383                    }
    384                }
    385            }
    386            catch (IOException e)
    387            {
    388                e.printStackTrace();
    389            }
    390            finally
    391            {
    392                if (buffer != null)
    393                {
    394                    try
    395                    {
    396                        buffer.close();
    397                    } catch (IOException e){}
    398                }
    399            }
    400        }
    401
    402        public void save()
    403        {
    404            if (PARENT != null && PARENT != this)
    405            {
    406                PARENT.save();
    407                return;
    408            }
    409
    410            try
    411            {
    412                if (file.getParentFile() != null)
    413                {
    414                    file.getParentFile().mkdirs();
    415                }
    416
    417                if (!file.exists() && !file.createNewFile())
    418                {
    419                    return;
    420                }
    421
    422                if (file.canWrite())
    423                {
    424                    FileOutputStream fos = new FileOutputStream(file);
    425                    BufferedWriter buffer = new BufferedWriter(new OutputStreamWriter(fos, defaultEncoding));
    426
    427                    buffer.write("# Configuration file\r\n");
    428                    buffer.write("# Generated on " + DateFormat.getInstance().format(new Date()) + "\r\n");
    429                    buffer.write("\r\n");
    430
    431                    if (children.isEmpty())
    432                    {
    433                        save(buffer);
    434                    }
    435                    else
    436                    {
    437                        for (Map.Entry<String, Configuration> entry : children.entrySet())
    438                        {
    439                            buffer.write("START: \"" + entry.getKey() + "\"\r\n");
    440                            entry.getValue().save(buffer);
    441                            buffer.write("END: \"" + entry.getKey() + "\"\r\n\r\n");
    442                        }
    443                    }
    444
    445                    buffer.close();
    446                    fos.close();
    447                }
    448            }
    449            catch (IOException e)
    450            {
    451                e.printStackTrace();
    452            }
    453        }
    454
    455        private void save(BufferedWriter out) throws IOException
    456        {
    457            for(Map.Entry<String, Map<String, Property>> category : categories.entrySet())
    458            {
    459                out.write("####################\r\n");
    460                out.write("# " + category.getKey() + " \r\n");
    461                if (customCategoryComments.containsKey(category.getKey()))
    462                {
    463                    out.write("#===================\r\n");
    464                    String comment = customCategoryComments.get(category.getKey());
    465                    Splitter splitter = Splitter.onPattern("\r?\n");
    466                    for (String commentLine : splitter.split(comment))
    467                    {
    468                        out.write("# ");
    469                        out.write(commentLine+"\r\n");
    470                    }
    471                }
    472                out.write("####################\r\n\r\n");
    473
    474                String catKey = category.getKey();
    475                if (!allowedProperties.matchesAllOf(catKey))
    476                {
    477                    catKey = '"'+catKey+'"';
    478                }
    479                out.write(catKey + " {\r\n");
    480                writeProperties(out, category.getValue().values());
    481                out.write("}\r\n\r\n");
    482            }
    483        }
    484
    485        public void addCustomCategoryComment(String category, String comment)
    486        {
    487            if (!caseSensitiveCustomCategories)
    488                category = category.toLowerCase(Locale.ENGLISH);
    489            customCategoryComments.put(category, comment);
    490        }
    491
    492        private void writeProperties(BufferedWriter buffer, Collection<Property> props) throws IOException
    493        {
    494            for (Property property : props)
    495            {
    496                if (property.comment != null)
    497                {
    498                    Splitter splitter = Splitter.onPattern("\r?\n");
    499                    for (String commentLine : splitter.split(property.comment))
    500                    {
    501                        buffer.write("   # " + commentLine + "\r\n");
    502                    }
    503                }
    504                String propName = property.getName();
    505                if (!allowedProperties.matchesAllOf(propName))
    506                {
    507                    propName = '"'+propName+'"';
    508                }
    509                buffer.write("   " + propName + "=" + property.value);
    510                buffer.write("\r\n");
    511            }
    512        }
    513
    514        private void setChild(String name, Configuration child)
    515        {
    516            if (!children.containsKey(name))
    517            {
    518                children.put(name, child);
    519            }
    520            else
    521            {
    522                Configuration old = children.get(name);
    523                child.categories = old.categories;
    524                child.customCategoryComments = old.customCategoryComments;
    525                child.fileName = old.fileName;
    526            }
    527        }
    528
    529        public static void enableGlobalConfig()
    530        {
    531            PARENT = new Configuration(new File(Loader.instance().getConfigDir(), "global.cfg"));
    532            PARENT.load();
    533        }
    534
    535        public static class UnicodeInputStreamReader extends Reader
    536        {
    537            private final InputStreamReader input;
    538            private final String defaultEnc;
    539
    540            public UnicodeInputStreamReader(InputStream source, String encoding) throws IOException
    541            {
    542                defaultEnc = encoding;
    543                String enc = encoding;
    544                byte[] data = new byte[4];
    545
    546                PushbackInputStream pbStream = new PushbackInputStream(source, data.length);
    547                int read = pbStream.read(data, 0, data.length);
    548                int size = 0;
    549
    550                int bom16 = (data[0] & 0xFF) << 8 | (data[1] & 0xFF);
    551                int bom24 = bom16 << 8 | (data[2] & 0xFF);
    552                int bom32 = bom24 << 8 | (data[3] & 0xFF);
    553
    554                if (bom24 == 0xEFBBBF)
    555                {
    556                    enc = "UTF-8";
    557                    size = 3;
    558                }
    559                else if (bom16 == 0xFEFF)
    560                {
    561                    enc = "UTF-16BE";
    562                    size = 2;
    563                }
    564                else if (bom16 == 0xFFFE)
    565                {
    566                    enc = "UTF-16LE";
    567                    size = 2;
    568                }
    569                else if (bom32 == 0x0000FEFF)
    570                {
    571                    enc = "UTF-32BE";
    572                    size = 4;
    573                }
    574                else if (bom32 == 0xFFFE0000) //This will never happen as it'll be caught by UTF-16LE,
    575                {                             //but if anyone ever runs across a 32LE file, i'd like to disect it.
    576                    enc = "UTF-32LE";
    577                    size = 4;
    578                }
    579
    580                if (size < read)
    581                {
    582                    pbStream.unread(data, size, read - size);
    583                }
    584
    585                this.input = new InputStreamReader(pbStream, enc);
    586            }
    587
    588            public String getEncoding()
    589            {
    590                return input.getEncoding();
    591            }
    592
    593            @Override
    594            public int read(char[] cbuf, int off, int len) throws IOException
    595            {
    596                return input.read(cbuf, off, len);
    597            }
    598
    599            @Override
    600            public void close() throws IOException
    601            {
    602                input.close();
    603            }
    604        }
    605    }