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