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