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