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    }