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 ${<property name>}
080 * with property values.
081 * If property value contains ${<other property name>} 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 }