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