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}