1 | package com.hammurapi.common; |
2 | |
3 | import java.util.HashSet; |
4 | import java.util.Set; |
5 | import java.util.StringTokenizer; |
6 | |
7 | /** |
8 | * Expands tokens like $[myToken] to token values. |
9 | * Nested tokens are supported, i.e. if myToken value contains $[someOtherToken] |
10 | * then it will also be expanded. $[ can be escaped by adding extra $, e.g. $$[ will be replaced with $[. |
11 | * Token can contain default value separated from the token name by pipe, e.g. $[myToken|tokenDefaultValue]. |
12 | * @author Pavel Vlasov |
13 | */ |
14 | public class TokenExpander { |
15 | |
16 | /** |
17 | * Source of tokens. |
18 | * @author Pavel Vlasov |
19 | */ |
20 | public interface TokenSource { |
21 | |
22 | /** |
23 | * @param name Token name. |
24 | * @return Token value or null. |
25 | */ |
26 | String getToken(String name); |
27 | |
28 | } |
29 | |
30 | public static final String TOKEN_CLOSING_CHAR = "}"; |
31 | public static final String TOKEN_SECOND_OPENING_CHAR = "{"; |
32 | public static final String TOKEN_FIRST_OPENING_CHAR = "$"; |
33 | |
34 | private String tokenClosingChar = TOKEN_CLOSING_CHAR; |
35 | private String tokenSecondOpeningChar = TOKEN_SECOND_OPENING_CHAR; |
36 | private String tokenFirstOpeningChar = TOKEN_FIRST_OPENING_CHAR; |
37 | |
38 | private TokenSource tokens; |
39 | |
40 | /** |
41 | * Creates a new instance of PropertyParser |
42 | * @param properties Properties |
43 | * @param useNameAsDefault If true then property name will be used |
44 | * as property default value. |
45 | */ |
46 | public TokenExpander(TokenSource tokens) { |
47 | if (tokens==null) { |
48 | throw new NullPointerException("Tokens map is null"); |
49 | } |
50 | this.tokens=tokens; |
51 | } |
52 | |
53 | /** |
54 | * Creates a new instance of TokenExpander. |
55 | * @param tokens |
56 | * @param tokenFirstOpeningChar Token first opening char. Default is $. |
57 | * @param tokenSecondOpeningChar Token second opening char. Default is {. |
58 | * @param tokenClosingChar Token closing char. Default is }. |
59 | */ |
60 | public TokenExpander(TokenSource tokens, char tokenFirstOpeningChar, char tokenSecondOpeningChar, char tokenClosingChar) { |
61 | this(tokens); |
62 | this.tokenClosingChar=String.valueOf(tokenClosingChar); |
63 | this.tokenFirstOpeningChar=String.valueOf(tokenFirstOpeningChar); |
64 | this.tokenSecondOpeningChar=String.valueOf(tokenSecondOpeningChar); |
65 | } |
66 | |
67 | /** |
68 | * Property parsing. |
69 | * Replaces string ${<property name>} with property value. |
70 | * If property value contains ${<other property name>} it |
71 | * will be parsed. |
72 | */ |
73 | public String expandToken(String token, String defaultValue) throws TokenExpansionException { |
74 | return expandToken(token, defaultValue, new HashSet<String>()); |
75 | } |
76 | |
77 | /** |
78 | * Parses a string by replacing occurences of ${<property name>} |
79 | * with property values. |
80 | * If property value contains ${<other property name>} it |
81 | * will be parsed. |
82 | */ |
83 | public String expand(String str) throws TokenExpansionException { |
84 | return parse(str, null); |
85 | } |
86 | |
87 | private String expandToken(String token, String defaultValue, Set<String> stack) throws TokenExpansionException { |
88 | String value = tokens.getToken(token); |
89 | if (value==null) { |
90 | if (defaultValue==null) { |
91 | throw new TokenExpansionException("Token not found: "+token); |
92 | } |
93 | value = defaultValue; |
94 | } |
95 | |
96 | if (stack.contains(token)) { |
97 | throw new TokenExpansionException("Circular reference in token substitution, token: "+token); |
98 | } |
99 | |
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 | } |