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