001package com.randomnoun.common; 002 003/* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a 004 * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html) 005 */ 006import java.io.FileReader; 007import java.io.IOException; 008import java.io.InputStream; 009import java.io.InputStreamReader; 010import java.io.LineNumberReader; 011import java.io.OutputStream; 012import java.io.PrintStream; 013import java.io.PrintWriter; 014import java.io.Reader; 015import java.io.Writer; 016import java.nio.charset.Charset; 017import java.text.ParseException; 018import java.util.ArrayList; 019import java.util.Collection; 020import java.util.Enumeration; 021import java.util.InvalidPropertiesFormatException; 022import java.util.Iterator; 023import java.util.List; 024import java.util.Map; 025import java.util.Map.Entry; 026import java.util.Properties; 027import java.util.Set; 028import java.util.StringTokenizer; 029import java.util.function.BiConsumer; 030import java.util.function.BiFunction; 031import java.util.function.Function; 032 033import org.apache.log4j.Logger; 034 035/** 036 * The PropertyParser class parses a property definition text file into 037 * a Properties object. 038 * 039 * <p>This parser differs from the standard Properties parser in that 040 * sections can be marked off for particular regions (e.g. properties 041 * that only take effect in development, or when run on certain machines). 042 * The current region 043 * is specified by the <code>com.randomnoun.common.mode</code> system property 044 * set on the VM commandline. If this system property is not set, 045 * the region defaults to the value "<code>localhost-dev-unknown</code>". 046 * An alternate constructor exists if you 047 * wish to specify the region manually. 048 * 049 * <p>Environments are specified in '<i>machine-release-subsystem</i>' 050 * format, with each segment set as follows: 051 * <ul> 052 * <li>machine - the hostname of the system (in lowercase) 053 * <li>release - the development phase of the system (either set to <code>dev</code>, 054 * <code>xpt</code>, <code>prd</code> for development, acceptance or production 055 * <li>subsystem - the subsystem that this VM represents. 056 * </ul> 057 * 058 * <p>Properties are specified in the standard "propertyName=propertyValue" method. 059 * Whitespace is removed from either side of the '=' character. New-lines 060 * can be specified in the property value by using the escape sequence '\n'. 061 * Lines can be continued over a single line by placing the character '\' at 062 * at the end of the line to be continued; e.g. 063 * 064 * <pre style="code"> 065 * property1=value1 066 * property2=this is a very long value for property2, which spans \ 067 * over a single line. 068 * </pre> 069 * 070 * <p>Properties that are specific to a particular region should be surrounded 071 * by the lines "STARTENVIRONMENT environmentMask" and "ENDENVIRONMENT environmentMask". 072 * Individual properties can be defined for a region by prefixing the line 073 * with "ENV environmentMask"; e.g. 074 * 075 * <pre style="code"> 076 * property1=all regions 077 * 078 * STARTENVIRONMENT *-xpt-* 079 * property2=this property only set in acceptance region 080 * property3=same for this property 081 * ENDENVIRONMENT 082 * 083 * STARTENVIRONMENT dtp11523-dev-* 084 * property2=these properties only set in the development region 085 * property3=running on the host dtp11523 086 * ENDENVIRONMENT 087 * 088 * ENV *-prd-* property4=this property only visible in production 089 * </pre> 090 * 091 * <p>As shown above, the '<code>*</code>' character can be used to specify a property 092 * across multiple regions. The keywords 'STARTENVIRONMENT', 'ENDENVIRONMENT' and 093 * 'ENV' are case-insensitive 094 * 095 * <p>You can now also specify environments based on the values of previously-defined 096 * properties; this allows a simple <code>#ifdef</code> style facility. There are 097 * two types of syntax, which use regex matching or simple string matching; e.g. 098 * 099 * <pre style="code"> 100 * enable.fileAct=true 101 * compound.property=123-456-789 102 * 103 * STARTENVIRONMENT enable.fileAct = true 104 * # these properties are only set when enable.fileAct is set to true 105 * ENDENVIRONMENT 106 * STARTENVIRONMENT compound.property =~ *-789 107 * # these properties are only set when compound.property ends with -789 108 * ENDENVIRONMENT 109 * </pre> 110 * 111 * <p>Property files can contain any number of blank lines, or comments (lines 112 * starting with the '#' character), which will be ignored by the parser. 113 * 114 * <p>Any occurences of the string "<code>\n</code>" in a property value will be 115 * replaced by a newline character. 116 * 117 * <p>A property make also contain a List, rather than a string, by including the 118 * index of the list item in square brackets in the property key; e.g. 119 * 120 * <pre style="code"> 121 * listname[1]=first element 122 * listname[3]=third element 123 * </pre> 124 * 125 * <p>This implementation returns an ArrayList of Strings for these types of declarations. 126 * Undeclared array elements that appear before the last index will return null. 127 * 128 * <p>If you want to create a list, but the index of the elements is unknown (they are 129 * dependant on other properties, for example), then you can use a "*" to denote the 130 * next available list index, or leave it empty to denote the last used list index; e.g. 131 * 132 * <pre style="code"> 133 * testList[0].a=a-value 0 134 * testList[0].b=b-value 0 135 * testList[0].c=a-value 0 136 * 137 * testList[1].a=a-value 1 138 * testList[1].b=b-value 1 139 * testList[1].c=a-value 1 140 * 141 * testList[*].a=a-value 2 142 * testList[].b=b-value 2 143 * testList[].c=a-value 2 144 * </pre> 145 * 146 * will generate a three-element list, each of which contains a map with three key/value pairs. 147 * 148 * <p>Properties that appear multiple times will take the value of the last-specified 149 * value. 150 * 151 * 152 * @author knoxg 153 * 154 */ 155public class PropertyParser { 156 157 /** Logger instance for this class */ 158 public static final Logger logger = Logger.getLogger(PropertyParser.class.getName()); 159 160 /** display each token as it is read */ 161 static private final boolean verbose = false; 162 163 /** file to parse */ 164 private LineNumberReader lineReader; 165 166 /** line to parse */ 167 private StringTokenizer thisline; 168 169 /** environment processing enabled */ 170 private boolean inEnvironment = false; 171 172 /** currently within correct environment */ 173 private boolean correctEnvironment = true; 174 175 /** comment parsed so far */ 176 private String comment = null; 177 178 /** current environment string */ 179 private String environmentID = ""; 180 181 /** the properties object we will populate from this InputStream */ 182 private Properties properties = null; 183 184 private Properties propertyComments = null; 185 186 public static class PropertiesWithComments extends Properties { 187 /** Generated serialVersionUID */ 188 private static final long serialVersionUID = -780503047200669250L; 189 Properties p; 190 Properties c; 191 192 public PropertiesWithComments(Properties p, Properties c) { 193 this.p = p; 194 this.c = c; 195 } 196 197 public String getComment(String key) { 198 return c.getProperty(key); 199 } 200 201 public Properties getComments() { 202 return c; 203 } 204 205 206 public Object setProperty(String key, String value) { 207 return p.setProperty(key, value); 208 } 209 210 public void load(Reader reader) throws IOException { 211 p.load(reader); 212 } 213 214 public void load(InputStream inStream) throws IOException { 215 p.load(inStream); 216 } 217 218 /** @deprecated */ 219 public void save(OutputStream out, String comments) { 220 p.save(out, comments); 221 } 222 223 public void store(Writer writer, String comments) throws IOException { 224 p.store(writer, comments); 225 } 226 227 public void store(OutputStream out, String comments) throws IOException { 228 p.store(out, comments); 229 } 230 231 public void loadFromXML(InputStream in) throws IOException, InvalidPropertiesFormatException { 232 p.loadFromXML(in); 233 } 234 235 public void storeToXML(OutputStream os, String comment) throws IOException { 236 p.storeToXML(os, comment); 237 } 238 239 public void storeToXML(OutputStream os, String comment, String encoding) throws IOException { 240 p.storeToXML(os, comment, encoding); 241 } 242 243 public void storeToXML(OutputStream os, String comment, Charset charset) throws IOException { 244 p.storeToXML(os, comment, charset); 245 } 246 247 public String getProperty(String key) { 248 return p.getProperty(key); 249 } 250 251 public String getProperty(String key, String defaultValue) { 252 return p.getProperty(key, defaultValue); 253 } 254 255 public Enumeration<?> propertyNames() { 256 return p.propertyNames(); 257 } 258 259 public Set<String> stringPropertyNames() { 260 return p.stringPropertyNames(); 261 } 262 263 public void list(PrintStream out) { 264 p.list(out); 265 } 266 267 public void list(PrintWriter out) { 268 p.list(out); 269 } 270 271 public int size() { 272 return p.size(); 273 } 274 275 public boolean isEmpty() { 276 return p.isEmpty(); 277 } 278 279 public Enumeration<Object> keys() { 280 return p.keys(); 281 } 282 283 public Enumeration<Object> elements() { 284 return p.elements(); 285 } 286 287 public boolean contains(Object value) { 288 return p.contains(value); 289 } 290 291 public boolean containsValue(Object value) { 292 return p.containsValue(value); 293 } 294 295 public boolean containsKey(Object key) { 296 return p.containsKey(key); 297 } 298 299 public Object get(Object key) { 300 return p.get(key); 301 } 302 303 public Object put(Object key, Object value) { 304 return p.put(key, value); 305 } 306 307 public Object remove(Object key) { 308 return p.remove(key); 309 } 310 311 public void putAll(Map<?, ?> t) { 312 p.putAll(t); 313 } 314 315 public void clear() { 316 p.clear(); 317 } 318 319 public String toString() { 320 return p.toString(); 321 } 322 323 public Set<Object> keySet() { 324 return p.keySet(); 325 } 326 327 public Collection<Object> values() { 328 return p.values(); 329 } 330 331 public Set<java.util.Map.Entry<Object, Object>> entrySet() { 332 return p.entrySet(); 333 } 334 335 public boolean equals(Object o) { 336 return p.equals(o); 337 } 338 339 public int hashCode() { 340 return p.hashCode(); 341 } 342 343 public Object getOrDefault(Object key, Object defaultValue) { 344 return p.getOrDefault(key, defaultValue); 345 } 346 347 public void forEach(BiConsumer<? super Object, ? super Object> action) { 348 p.forEach(action); 349 } 350 351 public void replaceAll(BiFunction<? super Object, ? super Object, ?> function) { 352 p.replaceAll(function); 353 } 354 355 public Object putIfAbsent(Object key, Object value) { 356 return p.putIfAbsent(key, value); 357 } 358 359 public boolean remove(Object key, Object value) { 360 return p.remove(key, value); 361 } 362 363 public boolean replace(Object key, Object oldValue, Object newValue) { 364 return p.replace(key, oldValue, newValue); 365 } 366 367 public Object replace(Object key, Object value) { 368 return p.replace(key, value); 369 } 370 371 public Object computeIfAbsent(Object key, Function<? super Object, ?> mappingFunction) { 372 return p.computeIfAbsent(key, mappingFunction); 373 } 374 375 public Object computeIfPresent(Object key, BiFunction<? super Object, ? super Object, ?> remappingFunction) { 376 return p.computeIfPresent(key, remappingFunction); 377 } 378 379 public Object compute(Object key, BiFunction<? super Object, ? super Object, ?> remappingFunction) { 380 return p.compute(key, remappingFunction); 381 } 382 383 public Object merge(Object key, Object value, BiFunction<? super Object, ? super Object, ?> remappingFunction) { 384 return p.merge(key, value, remappingFunction); 385 } 386 387 public Object clone() { 388 return p.clone(); 389 } 390 391 } 392 393 /** 394 * Create a new Parser object. Note that parsing does not begin until the 395 * Parse method is called on this object. 396 * 397 * @param reader The source of the site definition text. 398 * @param environmentID The environment ID in which to parse this text. 399 */ 400 public PropertyParser(Reader reader, String environmentID) { 401 lineReader = new LineNumberReader(reader); 402 this.environmentID = environmentID; 403 } 404 405 /** 406 * Create a new Parser object. Note that parsing does not begin until the 407 * Parse method is called on this object. Assumes a DEV environment 408 * 409 * @param reader The source of the site definition text. 410 */ 411 public PropertyParser(Reader reader) { 412 this(reader, System.getProperty("com.randomnoun.common.mode", "localhost-dev-unknown")); 413 } 414 415 /** 416 * Generates a Properties object from the input stream. 417 * 418 * @return A valid Properties object. 419 * 420 * @throws IOException 421 * @throws ParseException 422 */ 423 public PropertiesWithComments parse() 424 throws ParseException, IOException { 425 String line = ""; // current line 426 String token = ""; // current token 427 428 // ensure that all class fields have been reset 429 properties = new Properties(); 430 propertyComments = new Properties(); 431 PropertiesWithComments pwc = new PropertiesWithComments(properties, propertyComments); 432 433 434 line = lineReader.readLine(); 435 436 while (line != null) { 437 line = line.trim(); 438 439 // line-continuation (a line ending with '\' is appended with the next) 440 // could prove hazardous if we get a \ on the last line 441 while (line != null && line.endsWith("\\")) { 442 line = line.substring(0, line.length() - 1); 443 444 try { 445 line = line + lineReader.readLine().trim(); 446 } catch (NullPointerException npe) { 447 // end of file reached; ignore 448 } 449 } 450 451 // true = don't return delimiters 452 thisline = new StringTokenizer(line, " =\t\n\r", true); 453 454 if (thisline.hasMoreTokens()) { 455 token = parseToken("keyword"); 456 457 // are we in the correct environment ? 458 if (correctEnvironment || token.toLowerCase().equals("endenvironment")) { 459 parseLine(token); 460 } 461 } 462 463 line = lineReader.readLine(); 464 } 465 466 return pwc; 467 } 468 469 /** 470 * Parses a single line of the site definition file. 471 * 472 * @param token The token that was at the beginning of this line 473 * 474 * @throws ParseException 475 */ 476 @SuppressWarnings("unchecked") 477 private void parseLine(String token) 478 throws ParseException { 479 String lowerCaseToken; 480 String nextToken = null; 481 String value = null; 482 // String comment = null; 483 484 // System.out.println("P " + token + "(" + line + ")"); 485 lowerCaseToken = token.toLowerCase(); 486 487 if (token.startsWith("##")) { 488 comment = (comment == null ? "" : comment + "\n" ) + parseTokenToEOL("property value"); 489 } else if (token.startsWith("#")) { 490 comment = (comment == null ? null : comment + "\n" + parseTokenToEOL("property value")); 491 } else if (lowerCaseToken.equals("startenvironment")) { 492 parseStartEnvironment(); 493 } else if (lowerCaseToken.equals("endenvironment")) { 494 parseEndEnvironment(); 495 } else if (lowerCaseToken.equals("env")) { 496 parseEnv(); 497 } else if (lowerCaseToken.equals("includeresource")) { 498 parseIncludeResource(); 499 } else { 500 // see if this is a key=value property assignment 501 try { 502 nextToken = parseToken("token"); 503 } catch (ParseException pe) { 504 newParseException("Unknown keyword '" + token + "', '=' expected"); 505 } 506 507 if (nextToken.equals("=")) { 508 // must be in a key=value assignment 509 value = null; 510 511 try { 512 value = parseTokenToEOL("property value"); 513 } catch (ParseException pe) { 514 } 515 516 if (value == null) { 517 value = ""; 518 } 519 520 // check if this is an array element 521 int lb = token.indexOf('['); 522 int rb = token.indexOf(']'); 523 524 525 if (lb != -1 || rb != -1) { 526 if (lb == -1 || rb == -1) { 527 newParseException("Invalid list property key '" + token + "'"); 528 } 529 List<Object> list = null; 530 String keyPart = token.substring(0, lb); 531 String listPart = token.substring(lb); 532 try { 533 list = (List<Object>) properties.get(keyPart); 534 } catch (ClassCastException cce) { 535 newParseException("Cannot create list '" + token + "', this property already exists"); 536 } 537 if (list == null) { 538 list = new ArrayList<Object>(); 539 properties.put(keyPart, list); 540 } 541 // if list index is "*" then create a new list index; if it's "-", then use the last index 542 String index = token.substring(lb+1, rb); 543 if (index.equals("*")) { 544 index = String.valueOf(list.size()); // 0-based list, so next index is set to the size 545 listPart = "[" + index + "]" + listPart.substring(listPart.indexOf("]")+1); 546 } else if (index.equals("")) { 547 index = String.valueOf(list.size()-1); 548 listPart = "[" + index + "]" + listPart.substring(listPart.indexOf("]")+1); 549 } 550 551 Struct.setValue(list, listPart, value, true, true, true); 552 // don't store comments in lists 553 comment = null; 554 } else { 555 // no index supplied, just set the property 556 properties.setProperty(token, value); 557 if (comment != null) { 558 propertyComments.setProperty(token, comment); 559 comment = null; 560 } 561 } 562 } else { 563 newParseException("Unknown token '" + nextToken + "', '=' expected"); 564 } 565 } 566 } 567 568 /** 569 * Creates and throws a new parse exception, which includes the current parse 570 * position within the Reader. 571 * 572 * @param s The additional text to be included in this exception 573 * 574 * @throws ParseException The parse exception requested 575 */ 576 private void newParseException(String s) 577 throws ParseException { 578 throw new ParseException("line " + lineReader.getLineNumber() + ": " + s, 0); 579 } 580 581 /** 582 * Returns the next token. 583 * 584 * @param what The type of token we are expecting. This string is only used 585 * in any parseExceptions which are thrown. 586 * 587 * @return The next token on the line. 588 * 589 * @throws ParseException A parsing exception has occurred. 590 */ 591 private String parseToken(String what) 592 throws ParseException { 593 String result = null; 594 595 // keep grabbing tokens, until we hit something that isn't considered whitespace 596 while (result == null || result.equals(" ") || result.equals("\r") || result.equals("\n") || result.equals("\t")) { 597 if (!thisline.hasMoreTokens()) { 598 newParseException("Expecting " + what); 599 } 600 601 result = thisline.nextToken(" =\t\n\r"); 602 } 603 604 // convert \n's to newlines. 605 result = result.replaceAll("\\\\n", "\n"); 606 607 if (verbose) { 608 logger.debug("parsed token: '" + result + "'"); 609 } 610 611 return result; 612 } 613 614 /** 615 * Grabs every token until the end of line, and returns it as a string. If the 616 * debugging variable 'verbose' is set to true, then each token is sent 617 * to System.out as it is read. Enclosing single or double-quotes are removed. 618 * 619 * @param what The type of token we are expecting. This text is used 620 * in any parseExceptions which are thrown. 621 * 622 * @return The remaining text. 623 * 624 * @throws ParseException 625 */ 626 private String parseTokenToEOL(String what) 627 throws ParseException { 628 String token; 629 630 if (!thisline.hasMoreTokens()) { 631 newParseException("Expecting " + what); 632 } 633 634 token = thisline.nextToken("\n").trim(); 635 token = token.replaceAll("\\\\n", "\n"); 636 637 if (verbose) { 638 System.out.println("parsed token: " + token); 639 } 640 641 return token; 642 } 643 644 /** 645 * Processes an ENV rule 646 * 647 * @throws ParseException 648 */ 649 private void parseEnv() 650 throws ParseException { 651 String selectedenvironmentID; 652 String token; 653 654 selectedenvironmentID = parseToken("Environment ID"); 655 656 // @TODO this should really use the same rules as parseStartEnvironment 657 if (selectedenvironmentID.equals(environmentID)) { 658 token = parseToken("keyword").toLowerCase(); 659 parseLine(token); 660 } 661 } 662 663 private void parseIncludeResource() throws ParseException { 664 // include another properties file in here. This has a very 665 // good chance of creating a infinite loop if one property file 666 // includes another one, which in turn includes the first one. 667 // - this will manifest itself in some kind of OutOfMemoryException 668 // or a StackOverflowException 669 670 String resourceName = parseTokenToEOL("resource name"); 671 logger.debug("Including property resource '" + resourceName + "'"); 672 673 // load properties from classpath (in .EAR) 674 InputStream inputStream = PropertyParser.class.getClassLoader().getResourceAsStream(resourceName); 675 if (inputStream==null) { 676 throw new ParseException("Could not find included resource '" + resourceName + "'", 0); 677 } 678 PropertyParser propertyParser = new PropertyParser(new InputStreamReader(inputStream), environmentID); 679 Properties includedProperties = new Properties(); 680 try { 681 includedProperties = propertyParser.parse(); 682 } catch (Exception e) { 683 throw (ParseException) new ParseException("Could not load included resource '" + 684 resourceName + "'", 0).initCause(e); 685 } 686 687 // should merge lists/maps, rather than replacing them. 688 // (maybe this should be configurable ?) 689 // @TODO code below only merges lists 690 for (Iterator<Entry<Object, Object>> i = includedProperties.entrySet().iterator(); i.hasNext(); ) { 691 Map.Entry<Object, Object> entry = i.next(); 692 if (entry.getValue() instanceof List) { 693 Object existingObj = properties.get(entry.getKey()); 694 if (existingObj==null || !(existingObj instanceof List)) { 695 properties.put(entry.getKey(), entry.getValue()); 696 } else { 697 @SuppressWarnings("unchecked") 698 List<Object> existingList = (List<Object>) existingObj; 699 @SuppressWarnings("unchecked") 700 List<Object> listValue = (List<Object>) entry.getValue(); 701 for (int j=0; j<listValue.size(); j++) { 702 if (listValue.get(j)!=null) { 703 Struct.setListElement(existingList, j, listValue.get(j)); 704 } 705 } 706 } 707 } else { 708 properties.put(entry.getKey(), entry.getValue()); 709 } 710 } 711 // properties.putAll(includedProperties); 712 } 713 714 /** Begins per-environment parsing rules. 715 * @throws ParseException 716 */ 717 private void parseStartEnvironment() 718 throws ParseException { 719 if (inEnvironment) { 720 newParseException("attempted to nest environment areas"); 721 } 722 String propertyName = "environmentId"; 723 String propertyMatch = null; 724 String propertyValue = null; 725 String envSpec = parseTokenToEOL("Environment specification"); 726 if (envSpec.indexOf("=~")!=-1) { 727 propertyName = envSpec.substring(0, envSpec.indexOf("=~")).trim(); 728 propertyMatch = envSpec.substring(envSpec.indexOf("=~")+2).trim(); 729 propertyMatch = propertyMatch.replaceAll("\\*", ".*"); 730 propertyValue = propertyName.equals("environmentId") ? environmentID : properties.getProperty(propertyName); 731 if (propertyValue==null) { propertyValue = ""; } 732 correctEnvironment = propertyValue.matches(propertyMatch); 733 } else if (envSpec.indexOf("=")!=-1) { 734 propertyName = envSpec.substring(0, envSpec.indexOf("=")).trim(); 735 propertyMatch = envSpec.substring(envSpec.indexOf("=")+1).trim(); 736 propertyValue = propertyName.equals("environmentId") ? environmentID : properties.getProperty(propertyName); 737 if (propertyValue==null) { propertyValue = ""; } 738 correctEnvironment = propertyValue.equals(propertyMatch); 739 } else { 740 // not on 'xxx=xxx' form, default to old behaviour (match on envId) 741 propertyMatch = envSpec.replaceAll("\\*", ".*"); 742 correctEnvironment = environmentID.toLowerCase().matches(propertyMatch.toLowerCase()); 743 } 744 745 // test for "property=value" style environments 746 //selectedenvironmentID = selectedenvironmentID.replaceAll("\\*", ".*"); 747 //correctEnvironment = environmentID.matches(selectedenvironmentID); 748 inEnvironment = true; 749 } 750 751 /** 752 * Completes per-environment parsing rules. 753 * 754 * @throws ParseException 755 */ 756 private void parseEndEnvironment() 757 throws ParseException { 758 if (!inEnvironment) { 759 newParseException("'endenvironment' without matching 'startenvironment'"); 760 } 761 762 inEnvironment = false; 763 correctEnvironment = true; 764 } 765 766 /** 767 * Returns a subset of a Properties object. The subset is determined by only 768 * returning those key/value pairs whose keys begin with a set prefix. e.g. 769 * if 'a' contains the properties 770 * 771 * <pre> 772 * customer.1.name=fish 773 * customer.1.description=Patagonian toothfish 774 * customer.2.name=hunter 775 * customer.2.description=Patagonian toothfish hunter 776 * customer.11.name=spear 777 * customer.11.description=Patagonian toothfish hunter's spear 778 * </pre> 779 * 780 * <p>then calling <code>restrict(a, "customer.1", false)</code> will return: 781 * 782 * <pre> 783 * customer.1.name=fish 784 * customer.1.description=Patagonian toothfish 785 * </pre> 786 * 787 * <p>setting the 'removePrefix' to true will remove the initial prefix from 788 * the returned property list; <code>restrict(a, "customer.1", true)</code> in 789 * this case will then return: 790 * 791 * <pre> 792 * name=fish 793 * description=Patagonian toothfish 794 * </pre> 795 * 796 * <p>Note that the prefix passed in to this method has no trailing period, 797 * but each property must contain that period (to prevent <code>customer.11</code> 798 * from being returned in the example above). 799 * 800 * <p>If 'properties' is set to null, then this method will return null. 801 * If 'prefix' is set to null, then this method will return the entire property list. 802 * 803 * @param properties The initial property list that we wish to restrict 804 * @param prefix The prefix used to restrict the property list 805 * @param removePrefix If set to true, the result keys will be stripped of the initial prefix text 806 * 807 * @return A restricted property list. 808 */ 809 public static Map<? extends Object, ? extends Object> restrict(Map<Object, Object> properties, String prefix, boolean removePrefix) { 810 if (properties == null) { 811 return null; 812 } 813 814 if (prefix == null) { 815 return properties; 816 } 817 818 Properties result = new Properties(); 819 820 // this could possibly break existing, yet weird code 821 // if (!prefix.endsWith(".")) { prefix = prefix + "."; }; 822 prefix = prefix + "."; 823 824 Map.Entry<Object, Object> entry; 825 826 for (Iterator<Entry<Object, Object>> i = properties.entrySet().iterator(); i.hasNext();) { 827 entry = i.next(); 828 829 String key = (String) entry.getKey(); 830 831 if (key.startsWith(prefix)) { 832 if (removePrefix) { 833 key = key.substring(prefix.length()); 834 } 835 836 result.put(key, entry.getValue()); 837 } 838 } 839 840 return result; 841 } 842 843 /** 844 * Method used to test the parser from the command line. The file to parse is 845 * specified on the command line; if missing, then it uses 'test.properties' 846 * as the default. 847 * 848 * @param args 849 * 850 * @throws IOException 851 * @throws ParseException 852 */ 853 public static void main(String[] args) 854 throws IOException, ParseException { 855 String filename = "test.properties"; 856 PropertyParser propertyParser; 857 858 if (args.length != 1) { 859 System.out.println("Reading from '" + filename + "' by default..."); 860 } else { 861 filename = args[0]; 862 } 863 864 try { 865 propertyParser = new PropertyParser(new FileReader(filename)); 866 propertyParser.parse(); 867 System.out.println("Parse OK"); 868 } catch (ParseException pe) { 869 System.out.println("Caught ParseException: " + pe); 870 pe.printStackTrace(); 871 } 872 } 873}