001    /**
002     * This software is provided under the terms of the Minecraft Forge Public
003     * License v1.0.
004     */
005    
006    package net.minecraftforge.common;
007    
008    import java.io.*;
009    import java.text.DateFormat;
010    import java.util.ArrayList;
011    import java.util.Arrays;
012    import java.util.Collection;
013    import java.util.Date;
014    import java.util.Locale;
015    import java.util.Map;
016    import java.util.TreeMap;
017    import java.util.regex.Matcher;
018    import java.util.regex.Pattern;
019    
020    import com.google.common.base.CharMatcher;
021    import com.google.common.base.Splitter;
022    import com.google.common.collect.Maps;
023    
024    import cpw.mods.fml.common.FMLCommonHandler;
025    import cpw.mods.fml.common.Loader;
026    import cpw.mods.fml.relauncher.FMLInjectionData;
027    
028    import net.minecraft.src.Block;
029    import net.minecraft.src.Item;
030    import static net.minecraftforge.common.Property.Type.*;
031    
032    /**
033     * This class offers advanced configurations capabilities, allowing to provide
034     * various categories for configuration variables.
035     */
036    public class Configuration
037    {
038        private static boolean[] configMarkers = new boolean[Item.itemsList.length];
039        private static final int ITEM_SHIFT = 256;
040        private static final int MAX_BLOCKS = 4096;
041    
042        public static final String CATEGORY_GENERAL = "general";
043        public static final String CATEGORY_BLOCK   = "block";
044        public static final String CATEGORY_ITEM    = "item";
045        public static final String ALLOWED_CHARS = "._-";
046        public static final String DEFAULT_ENCODING = "UTF-8";
047        public static final String CATEGORY_SPLITTER = ".";
048        public static final String NEW_LINE;
049        private static final Pattern CONFIG_START = Pattern.compile("START: \"([^\\\"]+)\"");
050        private static final Pattern CONFIG_END = Pattern.compile("END: \"([^\\\"]+)\"");
051        public static final CharMatcher allowedProperties = CharMatcher.JAVA_LETTER_OR_DIGIT.or(CharMatcher.anyOf(ALLOWED_CHARS));
052        private static Configuration PARENT = null;
053    
054        File file;
055    
056        public Map<String, ConfigCategory> categories = new TreeMap<String, ConfigCategory>();
057        private Map<String, Configuration> children = new TreeMap<String, Configuration>();
058    
059        private boolean caseSensitiveCustomCategories;
060        public String defaultEncoding = DEFAULT_ENCODING;
061        private String fileName = null;
062        public boolean isChild = false;
063    
064        static
065        {
066            Arrays.fill(configMarkers, false);
067            NEW_LINE = System.getProperty("line.separator");
068        }
069    
070        public Configuration(){}
071    
072        /**
073         * Create a configuration file for the file given in parameter.
074         */
075        public Configuration(File file)
076        {
077            this.file = file;
078            String basePath = ((File)(FMLInjectionData.data()[6])).getAbsolutePath().replace(File.separatorChar, '/').replace("/.", "");
079            String path = file.getAbsolutePath().replace(File.separatorChar, '/').replace("/./", "/").replace(basePath, "");
080            if (PARENT != null)
081            {
082                PARENT.setChild(path, this);
083                isChild = true;
084            }
085            else
086            {
087                fileName = path;
088                load();
089            }
090        }
091    
092        public Configuration(File file, boolean caseSensitiveCustomCategories)
093        {
094            this(file);
095            this.caseSensitiveCustomCategories = caseSensitiveCustomCategories;
096        }
097    
098        /**
099         * Gets or create a block id property. If the block id property key is
100         * already in the configuration, then it will be used. Otherwise,
101         * defaultId will be used, except if already taken, in which case this
102         * will try to determine a free default id.
103         */
104        public Property getBlock(String key, int defaultID)
105        {
106            return getBlock(CATEGORY_BLOCK, key, defaultID);
107        }
108    
109        public Property getBlock(String category, String key, int defaultID)
110        {
111            Property prop = get(category, key, -1);
112    
113            if (prop.getInt() != -1)
114            {
115                configMarkers[prop.getInt()] = true;
116                return prop;
117            }
118            else
119            {
120                if (Block.blocksList[defaultID] == null && !configMarkers[defaultID])
121                {
122                    prop.value = Integer.toString(defaultID);
123                    configMarkers[defaultID] = true;
124                    return prop;
125                }
126                else
127                {
128                    for (int j = Block.blocksList.length - 1; j > 0; j--)
129                    {
130                        if (Block.blocksList[j] == null && !configMarkers[j])
131                        {
132                            prop.value = Integer.toString(j);
133                            configMarkers[j] = true;
134                            return prop;
135                        }
136                    }
137    
138                    throw new RuntimeException("No more block ids available for " + key);
139                }
140            }
141        }
142    
143        public Property getItem(String key, int defaultID)
144        {
145            return getItem(CATEGORY_ITEM, key, defaultID);
146        }
147    
148        public Property getItem(String category, String key, int defaultID)
149        {
150            Property prop = get(category, key, -1);
151            int defaultShift = defaultID + ITEM_SHIFT;
152    
153            if (prop.getInt() != -1)
154            {
155                configMarkers[prop.getInt() + ITEM_SHIFT] = true;
156                return prop;
157            }
158            else
159            {
160                if (Item.itemsList[defaultShift] == null && !configMarkers[defaultShift] && defaultShift > Block.blocksList.length)
161                {
162                    prop.value = Integer.toString(defaultID);
163                    configMarkers[defaultShift] = true;
164                    return prop;
165                }
166                else
167                {
168                    for (int x = Item.itemsList.length - 1; x >= ITEM_SHIFT; x--)
169                    {
170                        if (Item.itemsList[x] == null && !configMarkers[x])
171                        {
172                            prop.value = Integer.toString(x - ITEM_SHIFT);
173                            configMarkers[x] = true;
174                            return prop;
175                        }
176                    }
177    
178                    throw new RuntimeException("No more item ids available for " + key);
179                }
180            }
181        }
182    
183        public Property get(String category, String key, int defaultValue)
184        {
185            Property prop = get(category, key, Integer.toString(defaultValue), INTEGER);
186            if (!prop.isIntValue())
187            {
188                prop.value = Integer.toString(defaultValue);
189            }
190            return prop;
191        }
192    
193        public Property get(String category, String key, boolean defaultValue)
194        {
195            Property prop = get(category, key, Boolean.toString(defaultValue), BOOLEAN);
196            if (!prop.isBooleanValue())
197            {
198                prop.value = Boolean.toString(defaultValue);
199            }
200            return prop;
201        }
202    
203        
204        public Property get(String category, String key, double defaultValue)
205        {
206            Property prop = get(category, key, Double.toString(defaultValue), DOUBLE);
207            if (!prop.isDoubleValue())
208            {
209                prop.value = Double.toString(defaultValue);
210            }
211            return prop;
212        }
213    
214        public Property get(String category, String key, String defaultValue)
215        {
216            return get(category, key, defaultValue, STRING);
217        }
218    
219        public Property get(String category, String key, String[] defaultValue)
220        {
221            return get(category, key, defaultValue, STRING);
222        }
223    
224        public Property get(String category, String key, int[] defaultValue)
225        {
226            String[] values = new String[defaultValue.length];
227            for (int i = 0; i < defaultValue.length; i++)
228            {
229                values[i] = Integer.toString(defaultValue[i]);
230            }
231    
232            Property prop =  get(category, key, values, INTEGER);
233            if (!prop.isIntList())
234            {
235                prop.valueList = values;
236            }
237    
238            return prop;
239        }
240    
241        public Property get(String category, String key, double[] defaultValue)
242        {
243            String[] values = new String[defaultValue.length];
244            for (int i = 0; i < defaultValue.length; i++)
245            {
246                values[i] = Double.toString(defaultValue[i]);
247            }
248    
249            Property prop =  get(category, key, values, DOUBLE);
250            
251            if (!prop.isDoubleList())
252            {
253                prop.valueList = values;
254            }
255    
256            return prop;
257        }
258    
259        public Property get(String category, String key, boolean[] defaultValue)
260        {
261            String[] values = new String[defaultValue.length];
262            for (int i = 0; i < defaultValue.length; i++)
263            {
264                values[i] = Boolean.toString(defaultValue[i]);
265            }
266    
267            Property prop =  get(category, key, values, BOOLEAN);
268            
269            if (!prop.isBooleanList())
270            {
271                prop.valueList = values;
272            }
273    
274            return prop;
275        }
276    
277        public Property get(String category, String key, String defaultValue, Property.Type type)
278        {
279            if (!caseSensitiveCustomCategories)
280            {
281                category = category.toLowerCase(Locale.ENGLISH);
282            }
283    
284            ConfigCategory cat = getCategory(category);
285    
286            if (cat.containsKey(key))
287            {
288                Property prop = cat.get(key);
289    
290                if (prop.getType() == null)
291                {
292                    prop = new Property(prop.getName(), prop.value, type);
293                    cat.set(key, prop);
294                }
295    
296                return prop;
297            }
298            else if (defaultValue != null)
299            {
300                Property prop = new Property(key, defaultValue, type);
301                cat.set(key, prop);
302                return prop;
303            }
304            else
305            {
306                return null;
307            }
308        }
309    
310        public Property get(String category, String key, String[] defaultValue, Property.Type type)
311        {
312            if (!caseSensitiveCustomCategories)
313            {
314                category = category.toLowerCase(Locale.ENGLISH);
315            }
316    
317            ConfigCategory cat = getCategory(category);
318    
319            if (cat.containsKey(key))
320            {
321                Property prop = cat.get(key);
322    
323                if (prop.getType() == null)
324                {
325                    prop = new Property(prop.getName(), prop.value, type);
326                    cat.set(key, prop);
327                }
328    
329                return prop;
330            }
331            else if (defaultValue != null)
332            {
333                Property prop = new Property(key, defaultValue, type);
334                cat.set(key, prop);
335                return prop;
336            }
337            else
338            {
339                return null;
340            }
341        }
342    
343        public boolean hasCategory(String category)
344        {
345            return categories.get(category) != null;
346        }
347    
348        public boolean hasKey(String category, String key)
349        {
350            ConfigCategory cat = categories.get(category);
351            return cat != null && cat.containsKey(key);
352        }
353    
354        public void load()
355        {
356            if (PARENT != null && PARENT != this)
357            {
358                return;
359            }
360    
361            BufferedReader buffer = null;
362            try
363            {
364                if (file.getParentFile() != null)
365                {
366                    file.getParentFile().mkdirs();
367                }
368    
369                if (!file.exists() && !file.createNewFile())
370                {
371                    return;
372                }
373    
374                if (file.canRead())
375                {
376                    UnicodeInputStreamReader input = new UnicodeInputStreamReader(new FileInputStream(file), defaultEncoding);
377                    defaultEncoding = input.getEncoding();
378                    buffer = new BufferedReader(input);
379    
380                    String line;
381                    ConfigCategory currentCat = null;
382                    Property.Type type = null;
383                    ArrayList<String> tmpList = null;
384                    int lineNum = 0;
385                    String name = null;
386    
387                    while (true)
388                    {
389                        lineNum++;
390                        line = buffer.readLine();
391    
392                        if (line == null)
393                        {
394                            break;
395                        }
396    
397                        Matcher start = CONFIG_START.matcher(line);
398                        Matcher end = CONFIG_END.matcher(line);
399    
400                        if (start.matches())
401                        {
402                            fileName = start.group(1);
403                            categories = new TreeMap<String, ConfigCategory>();
404                            continue;
405                        }
406                        else if (end.matches())
407                        {
408                            fileName = end.group(1);
409                            Configuration child = new Configuration();
410                            child.categories = categories;
411                            this.children.put(fileName, child);
412                            continue;
413                        }
414    
415                        int nameStart = -1, nameEnd = -1;
416                        boolean skip = false;
417                        boolean quoted = false;
418    
419                        for (int i = 0; i < line.length() && !skip; ++i)
420                        {
421                            if (Character.isLetterOrDigit(line.charAt(i)) || ALLOWED_CHARS.indexOf(line.charAt(i)) != -1 || (quoted && line.charAt(i) != '"'))
422                            {
423                                if (nameStart == -1)
424                                {
425                                    nameStart = i;
426                                }
427    
428                                nameEnd = i;
429                            }
430                            else if (Character.isWhitespace(line.charAt(i)))
431                            {
432                                // ignore space charaters
433                            }
434                            else
435                            {
436                                switch (line.charAt(i))
437                                {
438                                    case '#':
439                                        skip = true;
440                                        continue;
441    
442                                    case '"':
443                                        if (quoted)
444                                        {
445                                            quoted = false;
446                                        }
447                                        if (!quoted && nameStart == -1)
448                                        {
449                                            quoted = true;
450                                        }
451                                        break;
452    
453                                    case '{':
454                                        name = line.substring(nameStart, nameEnd + 1);
455                                        String qualifiedName = ConfigCategory.getQualifiedName(name, currentCat);
456    
457                                        currentCat = categories.get(qualifiedName);
458                                        if (currentCat == null)
459                                        {
460                                            currentCat = new ConfigCategory(name, null);
461                                            categories.put(qualifiedName, currentCat);
462                                        }
463                                        name = null;
464    
465                                        break;
466    
467                                    case '}':
468                                        currentCat = currentCat.parent;
469                                        break;
470    
471                                    case '=':
472                                        name = line.substring(nameStart, nameEnd + 1);
473    
474                                        if (currentCat == null)
475                                        {
476                                            throw new RuntimeException(String.format("'%s' has no scope in '%s:%d'", name, fileName, lineNum));
477                                        }
478    
479                                        Property prop = new Property(name, line.substring(i + 1), type, true);
480                                        i = line.length();
481    
482                                        currentCat.set(name, prop);
483    
484                                        break;
485    
486                                    case ':':
487                                        type = Property.Type.tryParse(line.substring(nameStart, nameEnd + 1).charAt(0));
488                                        nameStart = nameEnd = -1;
489                                        break;
490    
491                                    case '<':
492                                        if (tmpList != null)
493                                        {
494                                            throw new RuntimeException(String.format("Malformed list property \"%s:%d\"", fileName, lineNum));
495                                        }
496    
497                                        name = line.substring(nameStart, nameEnd + 1);
498    
499                                        if (currentCat == null)
500                                        {
501                                            throw new RuntimeException(String.format("'%s' has no scope in '%s:%d'", name, fileName, lineNum));
502                                        }
503    
504                                        tmpList = new ArrayList<String>();
505    
506                                        break;
507    
508                                    case '>':
509                                        if (tmpList == null)
510                                        {
511                                            throw new RuntimeException(String.format("Malformed list property \"%s:%d\"", fileName, lineNum));
512                                        }
513    
514                                        currentCat.set(name, new Property(name, tmpList.toArray(new String[tmpList.size()]), type));
515                                        name = null;
516                                        tmpList = null;
517                                        type = null;
518                                        break;
519    
520                                    default:
521                                        throw new RuntimeException(String.format("Unknown character '%s' in '%s:%d'", line.charAt(i), fileName, lineNum));
522                                }
523                            }
524                        }
525    
526                        if (quoted)
527                        {
528                            throw new RuntimeException(String.format("Unmatched quote in '%s:%d'", fileName, lineNum));
529                        }
530                        else if (tmpList != null)
531                        {
532                            tmpList.add(line.trim());
533                        }
534                    }
535                }
536            }
537            catch (IOException e)
538            {
539                e.printStackTrace();
540            }
541            finally
542            {
543                if (buffer != null)
544                {
545                    try
546                    {
547                        buffer.close();
548                    } catch (IOException e){}
549                }
550            }
551        }
552    
553        public void save()
554        {
555            if (PARENT != null && PARENT != this)
556            {
557                PARENT.save();
558                return;
559            }
560    
561            try
562            {
563                if (file.getParentFile() != null)
564                {
565                    file.getParentFile().mkdirs();
566                }
567    
568                if (!file.exists() && !file.createNewFile())
569                {
570                    return;
571                }
572    
573                if (file.canWrite())
574                {
575                    FileOutputStream fos = new FileOutputStream(file);
576                    BufferedWriter buffer = new BufferedWriter(new OutputStreamWriter(fos, defaultEncoding));
577    
578                    buffer.write("# Configuration file" + NEW_LINE);
579                    buffer.write("# Generated on " + DateFormat.getInstance().format(new Date()) + NEW_LINE + NEW_LINE);
580    
581                    if (children.isEmpty())
582                    {
583                        save(buffer);
584                    }
585                    else
586                    {
587                        for (Map.Entry<String, Configuration> entry : children.entrySet())
588                        {
589                            buffer.write("START: \"" + entry.getKey() + "\"" + NEW_LINE);
590                            entry.getValue().save(buffer);
591                            buffer.write("END: \"" + entry.getKey() + "\"" + NEW_LINE + NEW_LINE);
592                        }
593                    }
594    
595                    buffer.close();
596                    fos.close();
597                }
598            }
599            catch (IOException e)
600            {
601                e.printStackTrace();
602            }
603        }
604    
605        private void save(BufferedWriter out) throws IOException
606        {
607            for (ConfigCategory cat : categories.values())
608            {
609                if (!cat.isChild())
610                {
611                    cat.write(out, 0);
612                    out.newLine();
613                }
614            }
615        }
616    
617        public ConfigCategory getCategory(String category)
618        {
619            ConfigCategory ret = categories.get(category);
620    
621            if (ret == null)
622            {
623                if (category.contains(CATEGORY_SPLITTER))
624                {
625                    String[] hierarchy = category.split("\\"+CATEGORY_SPLITTER);
626                    ConfigCategory parent = categories.get(hierarchy[0]);
627    
628                    if (parent == null)
629                    {
630                        parent = new ConfigCategory(hierarchy[0]);
631                        categories.put(parent.getQualifiedName(), parent);
632                    }
633    
634                    for (int i = 1; i < hierarchy.length; i++)
635                    {
636                        String name = ConfigCategory.getQualifiedName(hierarchy[i], parent);
637                        ConfigCategory child = categories.get(name);
638    
639                        if (child == null)
640                        {
641                            child = new ConfigCategory(hierarchy[i], parent);
642                            categories.put(name, child);
643                        }
644    
645                        ret = child;
646                        parent = child;
647                    }
648                }
649                else
650                {
651                    ret = new ConfigCategory(category);
652                    categories.put(category, ret);
653                }
654            }
655    
656            return ret;
657        }
658    
659        public void addCustomCategoryComment(String category, String comment)
660        {
661            if (!caseSensitiveCustomCategories)
662                category = category.toLowerCase(Locale.ENGLISH);
663            getCategory(category).setComment(comment);
664        }
665    
666        private void setChild(String name, Configuration child)
667        {
668            if (!children.containsKey(name))
669            {
670                children.put(name, child);
671            }
672            else
673            {
674                Configuration old = children.get(name);
675                child.categories = old.categories;
676                child.fileName = old.fileName;
677            }
678        }
679    
680        public static void enableGlobalConfig()
681        {
682            PARENT = new Configuration(new File(Loader.instance().getConfigDir(), "global.cfg"));
683            PARENT.load();
684        }
685    
686        public static class UnicodeInputStreamReader extends Reader
687        {
688            private final InputStreamReader input;
689            private final String defaultEnc;
690    
691            public UnicodeInputStreamReader(InputStream source, String encoding) throws IOException
692            {
693                defaultEnc = encoding;
694                String enc = encoding;
695                byte[] data = new byte[4];
696    
697                PushbackInputStream pbStream = new PushbackInputStream(source, data.length);
698                int read = pbStream.read(data, 0, data.length);
699                int size = 0;
700    
701                int bom16 = (data[0] & 0xFF) << 8 | (data[1] & 0xFF);
702                int bom24 = bom16 << 8 | (data[2] & 0xFF);
703                int bom32 = bom24 << 8 | (data[3] & 0xFF);
704    
705                if (bom24 == 0xEFBBBF)
706                {
707                    enc = "UTF-8";
708                    size = 3;
709                }
710                else if (bom16 == 0xFEFF)
711                {
712                    enc = "UTF-16BE";
713                    size = 2;
714                }
715                else if (bom16 == 0xFFFE)
716                {
717                    enc = "UTF-16LE";
718                    size = 2;
719                }
720                else if (bom32 == 0x0000FEFF)
721                {
722                    enc = "UTF-32BE";
723                    size = 4;
724                }
725                else if (bom32 == 0xFFFE0000) //This will never happen as it'll be caught by UTF-16LE,
726                {                             //but if anyone ever runs across a 32LE file, i'd like to disect it.
727                    enc = "UTF-32LE";
728                    size = 4;
729                }
730    
731                if (size < read)
732                {
733                    pbStream.unread(data, size, read - size);
734                }
735    
736                this.input = new InputStreamReader(pbStream, enc);
737            }
738    
739            public String getEncoding()
740            {
741                return input.getEncoding();
742            }
743    
744            @Override
745            public int read(char[] cbuf, int off, int len) throws IOException
746            {
747                return input.read(cbuf, off, len);
748            }
749    
750            @Override
751            public void close() throws IOException
752            {
753                input.close();
754            }
755        }
756    }