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 private Map<String,String> customCategoryComments = Maps.newHashMap(); 047 private boolean caseSensitiveCustomCategories; 048 public String defaultEncoding = DEFAULT_ENCODING; 049 050 static 051 { 052 Arrays.fill(configBlocks, false); 053 Arrays.fill(configItems, false); 054 } 055 056 /** 057 * Create a configuration file for the file given in parameter. 058 */ 059 public Configuration(File file) 060 { 061 this.file = file; 062 } 063 064 public Configuration(File file, boolean caseSensitiveCustomCategories) 065 { 066 this(file); 067 this.caseSensitiveCustomCategories = caseSensitiveCustomCategories; 068 } 069 070 /** 071 * Gets or create a block id property. If the block id property key is 072 * already in the configuration, then it will be used. Otherwise, 073 * defaultId will be used, except if already taken, in which case this 074 * will try to determine a free default id. 075 */ 076 public Property getBlock(String key, int defaultID) 077 { 078 return getBlock(CATEGORY_BLOCK, key, defaultID); 079 } 080 081 public Property getBlock(String category, String key, int defaultID) 082 { 083 Property prop = get(category, key, -1); 084 085 if (prop.getInt() != -1) 086 { 087 configBlocks[prop.getInt()] = true; 088 return prop; 089 } 090 else 091 { 092 if (Block.blocksList[defaultID] == null && !configBlocks[defaultID]) 093 { 094 prop.value = Integer.toString(defaultID); 095 configBlocks[defaultID] = true; 096 return prop; 097 } 098 else 099 { 100 for (int j = configBlocks.length - 1; j > 0; j--) 101 { 102 if (Block.blocksList[j] == null && !configBlocks[j]) 103 { 104 prop.value = Integer.toString(j); 105 configBlocks[j] = true; 106 return prop; 107 } 108 } 109 110 throw new RuntimeException("No more block ids available for " + key); 111 } 112 } 113 } 114 115 public Property getItem(String key, int defaultID) 116 { 117 return getItem(CATEGORY_ITEM, key, defaultID); 118 } 119 120 public Property getItem(String category, String key, int defaultID) 121 { 122 Property prop = get(category, key, -1); 123 int defaultShift = defaultID + ITEM_SHIFT; 124 125 if (prop.getInt() != -1) 126 { 127 configItems[prop.getInt() + ITEM_SHIFT] = true; 128 return prop; 129 } 130 else 131 { 132 if (Item.itemsList[defaultShift] == null && !configItems[defaultShift] && defaultShift > Block.blocksList.length) 133 { 134 prop.value = Integer.toString(defaultID); 135 configItems[defaultShift] = true; 136 return prop; 137 } 138 else 139 { 140 for (int x = configItems.length - 1; x >= ITEM_SHIFT; x--) 141 { 142 if (Item.itemsList[x] == null && !configItems[x]) 143 { 144 prop.value = Integer.toString(x - ITEM_SHIFT); 145 configItems[x] = true; 146 return prop; 147 } 148 } 149 150 throw new RuntimeException("No more item ids available for " + key); 151 } 152 } 153 } 154 155 public Property get(String category, String key, int defaultValue) 156 { 157 Property prop = get(category, key, Integer.toString(defaultValue), INTEGER); 158 if (!prop.isIntValue()) 159 { 160 prop.value = Integer.toString(defaultValue); 161 } 162 return prop; 163 } 164 165 public Property get(String category, String key, boolean defaultValue) 166 { 167 Property prop = get(category, key, Boolean.toString(defaultValue), BOOLEAN); 168 if (!prop.isBooleanValue()) 169 { 170 prop.value = Boolean.toString(defaultValue); 171 } 172 return prop; 173 } 174 175 public Property get(String category, String key, String defaultValue) 176 { 177 return get(category, key, defaultValue, STRING); 178 } 179 180 public Property get(String category, String key, String defaultValue, Property.Type type) 181 { 182 if (!caseSensitiveCustomCategories) 183 { 184 category = category.toLowerCase(Locale.ENGLISH); 185 } 186 187 Map<String, Property> source = categories.get(category); 188 189 if(source == null) 190 { 191 source = new TreeMap<String, Property>(); 192 categories.put(category, source); 193 } 194 195 if (source.containsKey(key)) 196 { 197 return source.get(key); 198 } 199 else if (defaultValue != null) 200 { 201 Property prop = new Property(key, defaultValue, type); 202 source.put(key, prop); 203 return prop; 204 } 205 else 206 { 207 return null; 208 } 209 } 210 211 public boolean hasCategory(String category) 212 { 213 return categories.get(category) != null; 214 } 215 216 public boolean hasKey(String category, String key) 217 { 218 Map<String, Property> cat = categories.get(category); 219 return cat != null && cat.get(key) != null; 220 } 221 222 public void load() 223 { 224 BufferedReader buffer = null; 225 try 226 { 227 if (file.getParentFile() != null) 228 { 229 file.getParentFile().mkdirs(); 230 } 231 232 if (!file.exists() && !file.createNewFile()) 233 { 234 return; 235 } 236 237 if (file.canRead()) 238 { 239 UnicodeInputStreamReader input = new UnicodeInputStreamReader(new FileInputStream(file), defaultEncoding); 240 defaultEncoding = input.getEncoding(); 241 buffer = new BufferedReader(input); 242 243 String line; 244 Map<String, Property> currentMap = null; 245 246 while (true) 247 { 248 line = buffer.readLine(); 249 250 if (line == null) 251 { 252 break; 253 } 254 255 int nameStart = -1, nameEnd = -1; 256 boolean skip = false; 257 boolean quoted = false; 258 for (int i = 0; i < line.length() && !skip; ++i) 259 { 260 if (Character.isLetterOrDigit(line.charAt(i)) || ALLOWED_CHARS.indexOf(line.charAt(i)) != -1 || (quoted && line.charAt(i) != '"')) 261 { 262 if (nameStart == -1) 263 { 264 nameStart = i; 265 } 266 267 nameEnd = i; 268 } 269 else if (Character.isWhitespace(line.charAt(i))) 270 { 271 // ignore space charaters 272 } 273 else 274 { 275 switch (line.charAt(i)) 276 { 277 case '#': 278 skip = true; 279 continue; 280 281 case '"': 282 if (quoted) 283 { 284 quoted = false; 285 } 286 if (!quoted && nameStart == -1) 287 { 288 quoted = true; 289 } 290 break; 291 292 case '{': 293 String scopeName = line.substring(nameStart, nameEnd + 1); 294 295 currentMap = categories.get(scopeName); 296 if (currentMap == null) 297 { 298 currentMap = new TreeMap<String, Property>(); 299 categories.put(scopeName, currentMap); 300 } 301 302 break; 303 304 case '}': 305 currentMap = null; 306 break; 307 308 case '=': 309 String propertyName = line.substring(nameStart, nameEnd + 1); 310 311 if (currentMap == null) 312 { 313 throw new RuntimeException("property " + propertyName + " has no scope"); 314 } 315 316 Property prop = new Property(); 317 prop.setName(propertyName); 318 prop.value = line.substring(i + 1); 319 i = line.length(); 320 321 currentMap.put(propertyName, prop); 322 323 break; 324 325 default: 326 throw new RuntimeException("unknown character " + line.charAt(i)); 327 } 328 } 329 } 330 if (quoted) 331 { 332 throw new RuntimeException("unmatched quote"); 333 } 334 } 335 } 336 } 337 catch (IOException e) 338 { 339 e.printStackTrace(); 340 } 341 finally 342 { 343 if (buffer != null) 344 { 345 try 346 { 347 buffer.close(); 348 } catch (IOException e){} 349 } 350 } 351 } 352 353 public void save() 354 { 355 try 356 { 357 if (file.getParentFile() != null) 358 { 359 file.getParentFile().mkdirs(); 360 } 361 362 if (!file.exists() && !file.createNewFile()) 363 { 364 return; 365 } 366 367 if (file.canWrite()) 368 { 369 FileOutputStream fos = new FileOutputStream(file); 370 BufferedWriter buffer = new BufferedWriter(new OutputStreamWriter(fos, defaultEncoding)); 371 372 buffer.write("# Configuration file\r\n"); 373 buffer.write("# Generated on " + DateFormat.getInstance().format(new Date()) + "\r\n"); 374 buffer.write("\r\n"); 375 376 for(Map.Entry<String, Map<String, Property>> category : categories.entrySet()) 377 { 378 buffer.write("####################\r\n"); 379 buffer.write("# " + category.getKey() + " \r\n"); 380 if (customCategoryComments.containsKey(category.getKey())) 381 { 382 buffer.write("#===================\r\n"); 383 String comment = customCategoryComments.get(category.getKey()); 384 Splitter splitter = Splitter.onPattern("\r?\n"); 385 for (String commentLine : splitter.split(comment)) 386 { 387 buffer.write("# "); 388 buffer.write(commentLine+"\r\n"); 389 } 390 } 391 buffer.write("####################\r\n\r\n"); 392 393 String catKey = category.getKey(); 394 if (!allowedProperties.matchesAllOf(catKey)) 395 { 396 catKey = '"'+catKey+'"'; 397 } 398 buffer.write(catKey + " {\r\n"); 399 writeProperties(buffer, category.getValue().values()); 400 buffer.write("}\r\n\r\n"); 401 } 402 403 buffer.close(); 404 fos.close(); 405 } 406 } 407 catch (IOException e) 408 { 409 e.printStackTrace(); 410 } 411 } 412 413 public void addCustomCategoryComment(String category, String comment) 414 { 415 if (!caseSensitiveCustomCategories) 416 category = category.toLowerCase(Locale.ENGLISH); 417 customCategoryComments.put(category, comment); 418 } 419 420 private void writeProperties(BufferedWriter buffer, Collection<Property> props) throws IOException 421 { 422 for (Property property : props) 423 { 424 if (property.comment != null) 425 { 426 Splitter splitter = Splitter.onPattern("\r?\n"); 427 for (String commentLine : splitter.split(property.comment)) 428 { 429 buffer.write(" # " + commentLine + "\r\n"); 430 } 431 } 432 String propName = property.getName(); 433 if (!allowedProperties.matchesAllOf(propName)) 434 { 435 propName = '"'+propName+'"'; 436 } 437 buffer.write(" " + propName + "=" + property.value); 438 buffer.write("\r\n"); 439 } 440 } 441 442 public static class UnicodeInputStreamReader extends Reader 443 { 444 private final InputStreamReader input; 445 private final String defaultEnc; 446 447 public UnicodeInputStreamReader(InputStream source, String encoding) throws IOException 448 { 449 defaultEnc = encoding; 450 String enc = encoding; 451 byte[] data = new byte[4]; 452 453 PushbackInputStream pbStream = new PushbackInputStream(source, data.length); 454 int read = pbStream.read(data, 0, data.length); 455 int size = 0; 456 457 int bom16 = (data[0] & 0xFF) << 8 | (data[1] & 0xFF); 458 int bom24 = bom16 << 8 | (data[2] & 0xFF); 459 int bom32 = bom24 << 8 | (data[3] & 0xFF); 460 461 if (bom24 == 0xEFBBBF) 462 { 463 enc = "UTF-8"; 464 size = 3; 465 } 466 else if (bom16 == 0xFEFF) 467 { 468 enc = "UTF-16BE"; 469 size = 2; 470 } 471 else if (bom16 == 0xFFFE) 472 { 473 enc = "UTF-16LE"; 474 size = 2; 475 } 476 else if (bom32 == 0x0000FEFF) 477 { 478 enc = "UTF-32BE"; 479 size = 4; 480 } 481 else if (bom32 == 0xFFFE0000) //This will never happen as it'll be caught by UTF-16LE, 482 { //but if anyone ever runs across a 32LE file, i'd like to disect it. 483 enc = "UTF-32LE"; 484 size = 4; 485 } 486 487 if (size < read) 488 { 489 pbStream.unread(data, size, read - size); 490 } 491 492 this.input = new InputStreamReader(pbStream, enc); 493 } 494 495 public String getEncoding() 496 { 497 return input.getEncoding(); 498 } 499 500 @Override 501 public int read(char[] cbuf, int off, int len) throws IOException 502 { 503 return input.read(cbuf, off, len); 504 } 505 506 @Override 507 public void close() throws IOException 508 { 509 input.close(); 510 } 511 } 512 }