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 }