001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.svl; 014 015import static org.apache.juneau.internal.StringUtils.*; 016 017import java.io.*; 018import java.lang.reflect.*; 019import java.util.*; 020 021import org.apache.juneau.internal.*; 022 023/** 024 * A var resolver session that combines a {@link VarResolver} with one or more session objects. 025 * 026 * <p> 027 * Instances of this class are considered light-weight and fast to construct, use, and discard. 028 * 029 * <p> 030 * This class contains the workhorse code for var resolution. 031 * 032 * <p> 033 * Instances of this class are created through the {@link VarResolver#createSession()} and 034 * {@link VarResolver#createSession(Map)} methods. 035 * 036 * <p> 037 * Instances of this class are NOT guaranteed to be thread safe. 038 * 039 * <h5 class='section'>See Also:</h5> 040 * <ul> 041 * <li class='link'>{@doc juneau-svl.VarResolvers} 042 * </ul> 043 */ 044public class VarResolverSession { 045 046 private final VarResolverContext context; 047 private final Map<String,Object> sessionObjects; 048 049 /** 050 * Constructor. 051 * 052 * @param context 053 * The {@link VarResolver} context object that contains the {@link Var Vars} and context objects associated with 054 * that resolver. 055 * @param sessionObjects The session objects. 056 * 057 */ 058 public VarResolverSession(VarResolverContext context, Map<String,Object> sessionObjects) { 059 this.context = context; 060 this.sessionObjects = sessionObjects != null ? sessionObjects : new HashMap<String,Object>(); 061 } 062 063 /** 064 * Adds a session object to this session. 065 * 066 * @param name The name of the session object. 067 * @param o The session object. 068 * @return This method (for method chaining). 069 */ 070 public VarResolverSession sessionObject(String name, Object o) { 071 sessionObjects.put(name, o); 072 return this; 073 } 074 075 /** 076 * Resolve all variables in the specified string. 077 * 078 * @param s 079 * The string to resolve variables in. 080 * @return 081 * The new string with all variables resolved, or the same string if no variables were found. 082 * <br>Returns <jk>null</jk> if the input was <jk>null</jk>. 083 */ 084 public String resolve(String s) { 085 086 if (s == null || s.isEmpty()) 087 return s; 088 089 if (s.indexOf('$') == -1 && s.indexOf('\\') == -1) 090 return s; 091 092 // Special case where value consists of a single variable with no embedded variables (e.g. "$X{...}"). 093 // This is a common case, so we want an optimized solution that doesn't involve string builders. 094 if (isSimpleVar(s)) { 095 String var = s.substring(1, s.indexOf('{')); 096 String val = s.substring(s.indexOf('{')+1, s.length()-1); 097 Var v = getVar(var); 098 if (v != null) { 099 try { 100 if (v.streamed) { 101 StringWriter sw = new StringWriter(); 102 v.resolveTo(this, sw, val); 103 return sw.toString(); 104 } 105 s = v.doResolve(this, val); 106 if (s == null) 107 s = ""; 108 return (v.allowRecurse() ? resolve(s) : s); 109 } catch (VarResolverException e) { 110 throw e; 111 } catch (Exception e) { 112 throw new VarResolverException(e, "Problem occurred resolving variable ''{0}'' in string ''{1}''", var, s); 113 } 114 } 115 return s; 116 } 117 118 try { 119 return resolveTo(s, new StringWriter()).toString(); 120 } catch (IOException e) { 121 throw new RuntimeException(e); // Never happens. 122 } 123 } 124 125 /** 126 * Convenience method for resolving variables in arbitrary objects. 127 * 128 * <p> 129 * Supports resolving variables in the following object types: 130 * <ul> 131 * <li>{@link CharSequence} 132 * <li>Arrays containing values of type {@link CharSequence}. 133 * <li>Collections containing values of type {@link CharSequence}. 134 * <br>Collection class must have a no-arg constructor. 135 * <li>Maps containing values of type {@link CharSequence}. 136 * <br>Map class must have a no-arg constructor. 137 * </ul> 138 * 139 * @param o The object. 140 * @return The same object if no resolution was needed, otherwise a new object or data structure if resolution was 141 * needed. 142 */ 143 @SuppressWarnings({ "rawtypes", "unchecked" }) 144 public <T> T resolve(T o) { 145 if (o == null) 146 return null; 147 if (o instanceof CharSequence) 148 return (T)resolve(o.toString()); 149 if (o.getClass().isArray()) { 150 if (! containsVars(o)) 151 return o; 152 Object o2 = Array.newInstance(o.getClass().getComponentType(), Array.getLength(o)); 153 for (int i = 0; i < Array.getLength(o); i++) 154 Array.set(o2, i, resolve(Array.get(o, i))); 155 return (T)o2; 156 } 157 if (o instanceof Collection) { 158 try { 159 Collection c = (Collection)o; 160 if (! containsVars(c)) 161 return o; 162 Collection c2 = c.getClass().newInstance(); 163 for (Object o2 : c) 164 c2.add(resolve(o2)); 165 return (T)c2; 166 } catch (VarResolverException e) { 167 throw e; 168 } catch (Exception e) { 169 throw new VarResolverException(e, "Problem occurred resolving collection."); 170 } 171 } 172 if (o instanceof Map) { 173 try { 174 Map m = (Map)o; 175 if (! containsVars(m)) 176 return o; 177 Map m2 = m.getClass().newInstance(); 178 for (Map.Entry e : (Set<Map.Entry>)m.entrySet()) 179 m2.put(e.getKey(), resolve(e.getValue())); 180 return (T)m2; 181 } catch (VarResolverException e) { 182 throw e; 183 } catch (Exception e) { 184 throw new VarResolverException(e, "Problem occurred resolving map."); 185 } 186 } 187 return o; 188 } 189 190 private static boolean containsVars(Object array) { 191 for (int i = 0; i < Array.getLength(array); i++) { 192 Object o = Array.get(array, i); 193 if (o instanceof CharSequence && o.toString().contains("$")) 194 return true; 195 } 196 return false; 197 } 198 199 @SuppressWarnings("rawtypes") 200 private static boolean containsVars(Collection c) { 201 for (Object o : c) 202 if (o instanceof CharSequence && o.toString().contains("$")) 203 return true; 204 return false; 205 } 206 207 @SuppressWarnings("rawtypes") 208 private static boolean containsVars(Map m) { 209 for (Object o : m.values()) 210 if (o instanceof CharSequence && o.toString().contains("$")) 211 return true; 212 return false; 213 } 214 215 /* 216 * Checks to see if string is of the simple form "$X{...}" with no embedded variables. 217 * This is a common case, and we can avoid using StringWriters. 218 */ 219 private static boolean isSimpleVar(String s) { 220 int S1 = 1; // Not in variable, looking for $ 221 int S2 = 2; // Found $, Looking for { 222 int S3 = 3; // Found {, Looking for } 223 int S4 = 4; // Found } 224 225 int length = s.length(); 226 int state = S1; 227 for (int i = 0; i < length; i++) { 228 char c = s.charAt(i); 229 if (state == S1) { 230 if (c == '$') { 231 state = S2; 232 } else { 233 return false; 234 } 235 } else if (state == S2) { 236 if (c == '{') { 237 state = S3; 238 } else if (c < 'A' || c > 'z' || (c > 'Z' && c < 'a')) { // False trigger "$X " 239 return false; 240 } 241 } else if (state == S3) { 242 if (c == '}') 243 state = S4; 244 else if (c == '{' || c == '$') 245 return false; 246 } else if (state == S4) { 247 return false; 248 } 249 } 250 return state == S4; 251 } 252 253 /** 254 * Resolves variables in the specified string and sends the output to the specified writer. 255 * 256 * <p> 257 * More efficient than first parsing to a string and then serializing to the writer since this method doesn't need 258 * to construct a large string. 259 * 260 * @param s The string to resolve variables in. 261 * @param out The writer to write to. 262 * @return The same writer. 263 * @throws IOException 264 */ 265 public Writer resolveTo(String s, Writer out) throws IOException { 266 267 int S1 = 1; // Not in variable, looking for $ 268 int S2 = 2; // Found $, Looking for { 269 int S3 = 3; // Found {, Looking for } 270 271 int state = S1; 272 boolean isInEscape = false; 273 boolean hasInternalVar = false; 274 boolean hasInnerEscapes = false; 275 String varType = null; 276 String varVal = null; 277 int x = 0, x2 = 0; 278 int depth = 0; 279 int length = s.length(); 280 for (int i = 0; i < length; i++) { 281 char c = s.charAt(i); 282 if (state == S1) { 283 if (isInEscape) { 284 if (c == '\\' || c == '$') { 285 out.append(c); 286 } else { 287 out.append('\\').append(c); 288 } 289 isInEscape = false; 290 } else if (c == '\\') { 291 isInEscape = true; 292 } else if (c == '$') { 293 x = i; 294 x2 = i; 295 state = S2; 296 } else { 297 out.append(c); 298 } 299 } else if (state == S2) { 300 if (isInEscape) { 301 isInEscape = false; 302 } else if (c == '\\') { 303 hasInnerEscapes = true; 304 isInEscape = true; 305 } else if (c == '{') { 306 varType = s.substring(x+1, i); 307 x = i; 308 state = S3; 309 } else if (c < 'A' || c > 'z' || (c > 'Z' && c < 'a')) { // False trigger "$X " 310 if (hasInnerEscapes) 311 out.append(unEscapeChars(s.substring(x, i+1), AS1)); 312 else 313 out.append(s, x, i+1); 314 x = i + 1; 315 state = S1; 316 hasInnerEscapes = false; 317 } 318 } else if (state == S3) { 319 if (isInEscape) { 320 isInEscape = false; 321 } else if (c == '\\') { 322 isInEscape = true; 323 hasInnerEscapes = true; 324 } else if (c == '{') { 325 depth++; 326 hasInternalVar = true; 327 } else if (c == '}') { 328 if (depth > 0) { 329 depth--; 330 } else { 331 varVal = s.substring(x+1, i); 332 Var r = getVar(varType); 333 if (r == null) { 334 if (hasInnerEscapes) 335 out.append(unEscapeChars(s.substring(x2, i+1), AS2)); 336 else 337 out.append(s, x2, i+1); 338 x = i+1; 339 } else { 340 varVal = (hasInternalVar && r.allowNested() ? resolve(varVal) : varVal); 341 try { 342 if (r.streamed) 343 r.resolveTo(this, out, varVal); 344 else { 345 String replacement = r.doResolve(this, varVal); 346 if (replacement == null) 347 replacement = ""; 348 // If the replacement also contains variables, replace them now. 349 if (replacement.indexOf('$') != -1 && r.allowRecurse()) 350 replacement = resolve(replacement); 351 out.append(replacement); 352 } 353 } catch (VarResolverException e) { 354 throw e; 355 } catch (Exception e) { 356 throw new VarResolverException(e, "Problem occurred resolving variable ''{0}'' in string ''{1}''", varType, s); 357 } 358 x = i+1; 359 } 360 state = 1; 361 hasInnerEscapes = false; 362 } 363 } 364 } 365 } 366 if (isInEscape) 367 out.append('\\'); 368 else if (state == S2) 369 out.append('$').append(unEscapeChars(s.substring(x+1), AS1)); 370 else if (state == S3) 371 out.append('$').append(varType).append('{').append(unEscapeChars(s.substring(x+1), AS2)); 372 return out; 373 } 374 375 private static final AsciiSet 376 AS1 = AsciiSet.create("\\{"), 377 AS2 = AsciiSet.create("\\${}") 378 ; 379 380 /** 381 * Returns the session object with the specified name. 382 * 383 * <p> 384 * Casts it to the specified class type for you. 385 * 386 * @param c The class type to cast to. 387 * @param name The name of the session object. 388 * @param throwNotSetException Throw a {@link VarResolverException} if the session object is not set. 389 * @return 390 * The session object. 391 * <br>Never <jk>null</jk>. 392 * @throws VarResolverException If session object with specified name does not exist. 393 */ 394 @SuppressWarnings("unchecked") 395 public <T> T getSessionObject(Class<T> c, String name, boolean throwNotSetException) { 396 T t = null; 397 try { 398 t = (T)sessionObjects.get(name); 399 if (t == null) { 400 sessionObjects.put(name, this.context.getContextObject(name)); 401 t = (T)sessionObjects.get(name); 402 } 403 } catch (Exception e) { 404 throw new VarResolverException(e, 405 "Session object ''{0}'' or context object ''SvlContext.{0}'' could not be converted to type ''{1}''.", name, c); 406 } 407 if (t == null && throwNotSetException) 408 throw new VarResolverException( 409 "Session object ''{0}'' or context object ''SvlContext.{0}'' not found.", name); 410 return t; 411 } 412 413 /** 414 * Returns the {@link Var} with the specified name. 415 * 416 * @param name The var name (e.g. <js>"S"</js>). 417 * @return The {@link Var} instance, or <jk>null</jk> if no <code>Var</code> is associated with the specified name. 418 */ 419 protected Var getVar(String name) { 420 return this.context.getVarMap().get(name); 421 } 422}