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