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