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