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