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);
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 = 4;
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                }
517                else if (bom32 == 0xFFFE0000) //This will never happen as it'll be caught by UTF-16LE,
518                {                             //but if anyone ever runs across a 32LE file, i'd like to disect it.
519                    enc = "UTF-32LE";
520                }
521    
522                if (size < read)
523                {
524                    pbStream.unread(data, size, read - size);
525                }
526    
527                this.input = new InputStreamReader(pbStream, enc);
528            }
529    
530            public String getEncoding()
531            {
532                return input.getEncoding();
533            }
534    
535            @Override
536            public int read(char[] cbuf, int off, int len) throws IOException
537            {
538                return input.read(cbuf, off, len);
539            }
540    
541            @Override
542            public void close() throws IOException
543            {
544                input.close();
545            }
546        }
547    }