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