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 }