    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 ${&lt;property name&gt;} with property value.
    070     * If property value contains ${&lt;other property name&gt;} 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}