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 }