View Javadoc
1   package com.randomnoun.common;
2   
3   /* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a
4    * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html)
5    */
6   import java.io.FileReader;
7   import java.io.IOException;
8   import java.io.InputStream;
9   import java.io.InputStreamReader;
10  import java.io.LineNumberReader;
11  import java.io.OutputStream;
12  import java.io.PrintStream;
13  import java.io.PrintWriter;
14  import java.io.Reader;
15  import java.io.Writer;
16  import java.nio.charset.Charset;
17  import java.text.ParseException;
18  import java.util.ArrayList;
19  import java.util.Collection;
20  import java.util.Enumeration;
21  import java.util.InvalidPropertiesFormatException;
22  import java.util.Iterator;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Map.Entry;
26  import java.util.Properties;
27  import java.util.Set;
28  import java.util.StringTokenizer;
29  import java.util.function.BiConsumer;
30  import java.util.function.BiFunction;
31  import java.util.function.Function;
32  
33  import org.apache.log4j.Logger;
34  
35  /**
36   * The PropertyParser class parses a property definition text file into
37   * a Properties object.
38   *
39   * <p>This parser differs from the standard Properties parser in that
40   * sections can be marked off for particular regions (e.g. properties
41   * that only take effect in development, or when run on certain machines).
42   * The current region
43   * is specified by the <code>com.randomnoun.common.mode</code> system property
44   * set on the VM commandline. If this system property is not set,
45   * the region defaults to the value "<code>localhost-dev-unknown</code>".
46   * An alternate constructor exists if you
47   * wish to specify the region manually.
48   *
49   * <p>Environments are specified in '<i>machine-release-subsystem</i>'
50   * format, with each segment set as follows:
51   * <ul>
52   * <li>machine - the hostname of the system (in lowercase)
53   * <li>release - the development phase of the system (either set to <code>dev</code>,
54   *   <code>xpt</code>, <code>prd</code> for development, acceptance or production
55   * <li>subsystem - the subsystem that this VM represents. 
56   * </ul>
57   *
58   * <p>Properties are specified in the standard "propertyName=propertyValue" method.
59   * Whitespace is removed from either side of the '=' character. New-lines
60   * can be specified in the property value by using the escape sequence '\n'.
61   * Lines can be continued over a single line by placing the character '\' at
62   * at the end of the line to be continued; e.g.
63   *
64   * <pre style="code">
65   *   property1=value1
66   *   property2=this is a very long value for property2, which spans \
67   *             over a single line.
68   * </pre>
69   *
70   * <p>Properties that are specific to a particular region should be surrounded
71   * by the lines "STARTENVIRONMENT environmentMask" and "ENDENVIRONMENT environmentMask".
72   * Individual properties can be defined for a region by prefixing the line
73   * with "ENV environmentMask"; e.g.
74   *
75   * <pre style="code">
76   *   property1=all regions
77   *
78   *   STARTENVIRONMENT *-xpt-*
79   *     property2=this property only set in acceptance region
80   *     property3=same for this property
81   *   ENDENVIRONMENT
82   *
83   *   STARTENVIRONMENT dtp11523-dev-*
84   *     property2=these properties only set in the development region
85   *     property3=running on the host dtp11523
86   *   ENDENVIRONMENT
87   *
88   *   ENV *-prd-* property4=this property only visible in production
89   * </pre>
90   *
91   * <p>As shown above, the '<code>*</code>' character can be used to specify a property
92   * across multiple regions. The keywords 'STARTENVIRONMENT', 'ENDENVIRONMENT' and
93   * 'ENV' are case-insensitive
94   * 
95   * <p>You can now also specify environments based on the values of previously-defined
96   * properties; this allows a simple <code>#ifdef</code> style facility. There are 
97   * two types of syntax, which use regex matching or simple string matching; e.g.
98   * 
99   * <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  */
155 public 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 }