001    /*
002    @license.text@
003     */
004    package biz.hammurapi.config;
005    
006    import java.util.HashSet;
007    import java.util.Set;
008    import java.util.StringTokenizer;
009    
010    /**
011     * Expands property value entries like ${myProperty} to property values.
012     * Nested properties are supportd, i.e. if myProperty value contains ${someOtherPropery}
013     * then it will also be expanded. ${ escaping is not supported.
014     * @author Pavel Vlasov
015     * @version $Revision: 1.3 $
016     */
017    public class PropertyParser {
018        public static final String TOKEN_CLOSING_CHAR = "}";
019            public static final String TOKEN_SECOND_OPENING_CHAR = "{";
020            public static final String TOKEN_FIRST_OPENING_CHAR = "$";
021            
022        private String tokenClosingChar = TOKEN_CLOSING_CHAR;
023            private String tokenSecondOpeningChar = TOKEN_SECOND_OPENING_CHAR;
024            private String tokenFirstOpeningChar = TOKEN_FIRST_OPENING_CHAR;
025            
026            private Context context;
027        private boolean useNameAsDefault;    
028        
029        /** 
030         * Creates a new instance of PropertyParser with system
031         * properties.
032         * @param useNameAsDefault If true then property name will be used
033         * as property default value. 
034         */
035        public PropertyParser(boolean useNameAsDefault) {
036            context=new Context() {
037                            public Object get(String name) {
038                                    return System.getProperty(name);
039                            }               
040            };
041            
042            this.useNameAsDefault=useNameAsDefault;
043        }
044        
045        /** 
046         * Creates a new instance of PropertyParser
047         * @param properties Properties
048         * @param useNameAsDefault If true then property name will be used
049         * as property default value. 
050         */
051        public PropertyParser(Context context, boolean useNameAsDefault) {
052            if (context==null) {
053                    throw new NullPointerException("Context is null");
054            }
055            this.context=context;
056            this.useNameAsDefault=useNameAsDefault;
057        }    
058        
059        /** 
060         * Creates a new instance of PropertyParser with system
061         * properties.
062         * @param useNameAsDefault If true then property name will be used
063         * as property default value. 
064         */
065        public PropertyParser(boolean useNameAsDefault, char tokenFirstOpeningChar, char tokenSecondOpeningChar, char tokenClosingChar) {
066            context=new Context() {
067                            public Object get(String name) {
068                                    return System.getProperty(name);
069                            }               
070            };
071            
072            this.useNameAsDefault=useNameAsDefault;
073            this.tokenClosingChar=String.valueOf(tokenClosingChar);
074            this.tokenFirstOpeningChar=String.valueOf(tokenFirstOpeningChar);
075            this.tokenSecondOpeningChar=String.valueOf(tokenSecondOpeningChar);
076        }
077        
078        /** 
079         * Creates a new instance of PropertyParser
080         * @param properties Properties
081         * @param useNameAsDefault If true then property name will be used
082         * as property default value. 
083         */
084        public PropertyParser(Context context, boolean useNameAsDefault, char tokenFirstOpeningChar, char tokenSecondOpeningChar, char tokenClosingChar) {
085            if (context==null) {
086                    throw new NullPointerException("Context is null");
087            }
088            this.context=context;
089            this.useNameAsDefault=useNameAsDefault;
090            this.tokenClosingChar=String.valueOf(tokenClosingChar);
091            this.tokenFirstOpeningChar=String.valueOf(tokenFirstOpeningChar);
092            this.tokenSecondOpeningChar=String.valueOf(tokenSecondOpeningChar);
093        }    
094        
095        
096        /**
097         * Property parsing. 
098         * Replaces string ${<property name>} with property value.
099         * If property value contains ${<other property name>} it
100         * will be parsed. 
101         */
102        public String getParsedProperty(String key) {
103            return getParsedProperty(key, new HashSet());
104        }
105        
106        /**
107         * Parses a string by replacing occurences of ${<property name>} 
108         * with property values.
109         * If property value contains ${<other property name>} it
110         * will be parsed. 
111         */
112        public String parse(String str) {
113            return parse(str, null);
114        }
115        
116        private String getParsedProperty(String key, Set stack) {
117            Object o=context.get(key);
118            String pr;        
119            if (o==null) {
120                    if (useNameAsDefault) {
121                            pr=key;
122                    } else {
123                            return null;
124                    }
125            } else {
126                    pr=o.toString();
127            }
128            
129            if (stack.contains(key)) {
130                    throw new RuntimeConfigurationException("Circular reference in property substitution, property: "+key);
131            }
132            
133            stack.add(key);
134            
135            return parse(pr, stack);
136        }
137        
138        private String parse(String str, Set stack) {
139            if (str==null) {
140                    return null;
141            }
142            
143            StringTokenizer st=new StringTokenizer(str,tokenFirstOpeningChar+tokenSecondOpeningChar+tokenClosingChar,true);
144            
145            StringBuffer ret=new StringBuffer();
146            /**
147             * Parser state:
148             * 0: Text
149             * 1: $
150             * 2: ${
151             */
152            int state=0;
153            
154            String propName=null;
155            while (st.hasMoreTokens()) {
156                String tkn=st.nextToken();
157                switch (state) {
158                    case 0:
159                        if (tokenFirstOpeningChar.equals(tkn))
160                            state=1;
161                        else
162                            ret.append(tkn);
163                        break;
164                    case 1:
165                        if (tokenSecondOpeningChar.equals(tkn))
166                            state=2;
167                        else {
168                            state=0;
169                            ret.append(tokenFirstOpeningChar);
170                            ret.append(tkn);
171                        }
172                        break;
173                    case 2:
174                        if (tokenClosingChar.equals(tkn)) {
175                            String propVal=getParsedProperty(propName, stack==null ? new HashSet() : stack);
176                            ret.append(propVal==null ? tokenFirstOpeningChar+tokenSecondOpeningChar+propName+tokenClosingChar : propVal);
177                            propName=null;
178                            state=0;
179                        } else {
180                            if (propName==null)
181                                propName=tkn;
182                            else
183                                propName+=tkn;
184                        }
185                        break;
186                    default:
187                        throw new IllegalStateException("Illegal parser state: "+state);
188                }
189            }
190            
191            if (state==2)
192                ret.append(tokenFirstOpeningChar+tokenSecondOpeningChar+propName);
193            
194            return ret.toString();
195        }        
196    }