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