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 }