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    }