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 }