001    package com.hammurapi.config.bootstrap;
002    
003    import java.util.HashSet;
004    import java.util.Set;
005    import java.util.StringTokenizer;
006    
007    
008    /**
009     * Expands tokens like $[myToken] to token values.
010     * Nested tokens are supported, i.e. if myToken value contains $[someOtherToken]
011     * then it will also be expanded. $[ can be escaped by adding extra $, e.g. $$[ will be replaced with $[.
012     * Token can contain default value separated from the token name by pipe, e.g. $[myToken|tokenDefaultValue].
013     * @author Pavel Vlasov
014     */
015    public class TokenExpander {
016            
017            /**
018             * Source of tokens.
019             * @author Pavel Vlasov
020             */
021            public interface TokenSource {
022                    
023                    /**
024                     * @param name Token name.
025                     * @return Token value or null.
026                     */
027                    String getToken(String name);
028    
029            }
030                    
031        public static final String TOKEN_CLOSING_CHAR = "}";
032            public static final String TOKEN_SECOND_OPENING_CHAR = "{";
033            public static final String TOKEN_FIRST_OPENING_CHAR = "$";
034            
035        private String tokenClosingChar = TOKEN_CLOSING_CHAR;
036            private String tokenSecondOpeningChar = TOKEN_SECOND_OPENING_CHAR;
037            private String tokenFirstOpeningChar = TOKEN_FIRST_OPENING_CHAR;
038            
039            private TokenSource tokens;
040        
041        /** 
042         * Creates a new instance of PropertyParser
043         * @param properties Properties
044         * @param useNameAsDefault If true then property name will be used
045         * as property default value. 
046         */
047        public TokenExpander(TokenSource tokens) {
048            if (tokens==null) {
049                    throw new NullPointerException("Tokens map is null");
050            }
051            this.tokens=tokens;
052        }    
053        
054        /** 
055         * Creates a new instance of TokenExpander.
056         * @param tokens 
057         * @param tokenFirstOpeningChar Token first opening char. Default is $.
058         * @param tokenSecondOpeningChar Token second opening char. Default is {.
059         * @param tokenClosingChar Token closing char. Default is }.
060         */
061        public TokenExpander(TokenSource tokens, char tokenFirstOpeningChar, char tokenSecondOpeningChar, char tokenClosingChar) {
062            this(tokens);
063            this.tokenClosingChar=String.valueOf(tokenClosingChar);
064            this.tokenFirstOpeningChar=String.valueOf(tokenFirstOpeningChar);
065            this.tokenSecondOpeningChar=String.valueOf(tokenSecondOpeningChar);
066        }
067                
068        /**
069         * Property parsing. 
070         * Replaces string ${<property name>} with property value.
071         * If property value contains ${<other property name>} it
072         * will be parsed. 
073         */
074        public String expandToken(String token, String defaultValue) throws ConfigurationException {
075            return expandToken(token, defaultValue, new HashSet<String>());
076        }
077        
078        /**
079         * Parses a string by replacing occurences of ${&lt;property name&gt;} 
080         * with property values.
081         * If property value contains ${&lt;other property name&gt;} it
082         * will be parsed. 
083         */
084        public String expand(String str) throws ConfigurationException {
085            return parse(str, null);
086        }
087    
088        private String expandToken(String token, String defaultValue, Set<String> stack) throws ConfigurationException {
089            String value = tokens.getToken(token);
090            if (value==null) {
091                    if (defaultValue==null) {
092                            throw new ConfigurationException("Token not found: "+token);
093                    } 
094                    value = defaultValue;
095            }
096            
097            if (stack.contains(token)) {
098                    throw new ConfigurationException("Circular reference in token substitution, token: "+token);
099            }
100            
101            stack.add(token);
102            
103            return parse(value, stack);
104        }
105        
106        private String parse(String str, Set<String> stack) throws ConfigurationException {
107            if (str==null) {
108                    return null;
109            }
110            
111            StringTokenizer st=new StringTokenizer(str,tokenFirstOpeningChar+tokenSecondOpeningChar+tokenClosingChar,true);
112            
113            StringBuilder ret=new StringBuilder();
114            
115            /**
116             * Parser state:
117             * 0: Text
118             * 1: $
119             * 2: ${
120             */
121            final int TEXT = 0;
122            final int OPEN_FIRST = 1;
123            final int OPEN_SECOND = 2;
124                    
125            int state=0;
126            
127            String propTxt=null;
128            while (st.hasMoreTokens()) {
129                String tkn=st.nextToken();
130                switch (state) {
131                    case TEXT:
132                        if (tokenFirstOpeningChar.equals(tkn))
133                            state=OPEN_FIRST;
134                        else
135                            ret.append(tkn);
136                        break;
137                    case OPEN_FIRST:
138                        if (tokenSecondOpeningChar.equals(tkn))
139                            state=OPEN_SECOND;
140                        else {
141                            state=TEXT;
142                            ret.append(tokenFirstOpeningChar);
143                            if (tkn.equals(tokenFirstOpeningChar)) {
144                                    String next = st.hasMoreTokens() ? st.nextToken() : null;
145                                    if (next==null) {
146                                            ret.append(tkn);
147                                    } else {
148                                            if (!next.equals(tokenSecondOpeningChar)) {
149                                                    ret.append(tkn);
150                                            }
151                                            ret.append(next);
152                                    }
153                            } else {
154                                    ret.append(tkn);
155                            }
156                        }
157                        break;
158                    case OPEN_SECOND:
159                        if (tokenClosingChar.equals(tkn)) {
160                            int pipeIdx = propTxt==null ? -1 : propTxt.indexOf('|');                        
161                            String propVal=expandToken(pipeIdx==-1 ? propTxt : propTxt.substring(0, pipeIdx), pipeIdx==-1 ? null : propTxt.substring(pipeIdx+1), stack==null ? new HashSet<String>() : stack);
162                            ret.append(propVal);
163                            propTxt=null;
164                            state=0;
165                        } else {
166                            if (propTxt==null) {
167                                propTxt=tkn;
168                            } else {
169                                propTxt+=tkn;
170                            }
171                        }
172                        break;
173                    default:
174                        throw new IllegalStateException("Illegal parser state: "+state);
175                }
176            }
177            
178            if (state==OPEN_SECOND) {
179                ret.append(tokenFirstOpeningChar+tokenSecondOpeningChar+propTxt);
180            }
181            
182            return ret.toString();
183        }        
184    }