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.commons.svl; 018 019import static org.apache.juneau.commons.reflect.ReflectionUtils.*; 020import static org.apache.juneau.commons.utils.ClassUtils.*; 021import static org.apache.juneau.commons.utils.CollectionUtils.*; 022import static org.apache.juneau.commons.utils.Shorts.*; 023import static org.apache.juneau.commons.utils.ThrowableUtils.*; 024 025import java.io.*; 026import java.lang.reflect.*; 027import java.util.*; 028import java.util.function.*; 029 030import org.apache.juneau.commons.collections.*; 031import org.apache.juneau.commons.inject.*; 032import org.apache.juneau.commons.lang.*; 033import org.apache.juneau.commons.utils.*; 034 035/** 036 * A var resolver session that combines a {@link VarResolver} with one or more session objects. 037 * 038 * <p> 039 * Instances of this class are considered light-weight and fast to construct, use, and discard. 040 * 041 * <p> 042 * This class contains the workhorse code for var resolution. 043 * 044 * <p> 045 * Instances of this class are created through the {@link VarResolver#createSession()} and 046 * {@link VarResolver#createSession(WritableBeanStore)} methods. 047 * 048 * <h5 class='section'>Notes:</h5><ul> 049 * <li class='warn'>This class is not guaranteed to be thread safe. 050 * </ul> 051 * 052 * <h5 class='section'>See Also:</h5><ul> 053 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/MarshallSimpleVariableLanguage">Marshall Simple Variable Language</a> 054 * </ul> 055 */ 056@SuppressWarnings({ 057 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 058 "resource" // VarResolver resources managed by calling code 059}) 060public class VarResolverSession { 061 062 private static boolean containsVars(Collection<?> c) { 063 var f = Flag.create(); 064 c.forEach(x -> { 065 if (x instanceof CharSequence && x.toString().contains("$")) 066 f.set(); 067 }); 068 return f.isSet(); 069 } 070 071 private static boolean containsVars(Map<?,?> m) { 072 var f = Flag.create(); 073 m.forEach((k, v) -> { 074 if (v instanceof CharSequence && v.toString().contains("$")) 075 f.set(); 076 }); 077 return f.isSet(); 078 } 079 080 private static boolean containsVars(Object array) { 081 for (var i = 0; i < Array.getLength(array); i++) { 082 var o = Array.get(array, i); 083 if (o instanceof CharSequence && o.toString().contains("$")) 084 return true; 085 } 086 return false; 087 } 088 089 private static final Set<String> HTML_DOC_PASSTHROUGH_VARS = Set.of("W", "C", "RS"); 090 091 private static final Set<String> SCRIPT_STYLE_REFUSED_VARS = Set.of("RQ", "RH", "RF", "RP", "RA", "SA", "R"); 092 093 private final VarResolver context; 094 095 private final WritableBeanStore beanStore; 096 097 private boolean htmlDocEncode; 098 099 private boolean scriptStyleRefuse; 100 101 /** 102 * Constructor. 103 * 104 * @param context 105 * The {@link VarResolver} context object that contains the {@link Var Vars} and context objects associated with 106 * that resolver. 107 * @param beanStore The bean store to use for resolving beans needed by vars. Can be <jk>null</jk> (this session will then have no parent bean store, and only beans explicitly added via {@link #bean(Class, Object)} will be resolvable). 108 * 109 */ 110 public VarResolverSession(VarResolver context, WritableBeanStore beanStore) { 111 this.context = context; 112 this.beanStore = new BasicBeanStore(beanStore); 113 } 114 115 /** 116 * Adds a bean to this session. 117 * 118 * @param <T> The bean type. 119 * @param c The bean type. 120 * @param value The bean. 121 * @return This object. 122 */ 123 public <T> VarResolverSession bean(Class<T> c, T value) { 124 beanStore.addBean(c, value); 125 return this; 126 } 127 128 /** 129 * Returns the bean from the registered bean store. 130 * 131 * @param <T> The value type. 132 * @param c The bean type. 133 * @return 134 * The bean. 135 * <br>Never <jk>null</jk>. 136 */ 137 public <T> Optional<T> getBean(Class<T> c) { 138 Optional<T> t = beanStore.getBean(c); 139 if (! t.isPresent()) 140 t = context.beanStore.getBean(c); 141 return t; 142 } 143 144 /** 145 * Resolve all variables in the specified string. 146 * 147 * @param s 148 * The string to resolve variables in. Can be <jk>null</jk> (returns <jk>null</jk>). 149 * @return 150 * The new string with all variables resolved, or the same string if no variables were found. 151 * <br>Returns <jk>null</jk> if the input was <jk>null</jk>. 152 */ 153 public String resolve(String s) { 154 if (s == null) 155 return null; 156 if (s.isEmpty()) 157 return s; 158 return context.compile(s).resolve(this); 159 } 160 161 /** 162 * Resolves SVL variables for HTML-doc chrome fields that write raw HTML. 163 * 164 * <p> 165 * Literal characters from the template (including tags) pass through unchanged. Each SVL 166 * substitution is XML-escaped via {@link StringUtils#escapeXml(String)} before insert, except 167 * the HTML-passthrough vars {@code $W}, {@code $C}, and {@code $RS}. 168 * 169 * <p> 170 * Plain {@link #resolve(String)} is unchanged. Use this only for HTML-doc chrome (aside, header, 171 * footer, nav HTML fragments, head, no-results). 172 * 173 * @param s The chrome template. Can be <jk>null</jk>. 174 * @return The resolved chrome string, or <jk>null</jk> if the input was <jk>null</jk>. 175 */ 176 public String resolveHtmlDoc(String s) { 177 htmlDocEncode = true; 178 try { 179 return resolve(s); 180 } finally { 181 htmlDocEncode = false; 182 } 183 } 184 185 /** 186 * Resolves SVL variables for {@code <script>} and {@code <style>} chrome. 187 * 188 * <p> 189 * Request-derived vars ({@code $RQ}, {@code $RH}, {@code $RF}, {@code $RP}, {@code $RA}, 190 * {@code $SA}, {@code $R}) are left unresolved (same as an unknown var). Other vars including 191 * {@code $W}, {@code $C}, and {@code $RS} still resolve. 192 * 193 * @param s The script or style template. Can be <jk>null</jk>. 194 * @return The resolved string, or <jk>null</jk> if the input was <jk>null</jk>. 195 */ 196 public String resolveScriptStyle(String s) { 197 scriptStyleRefuse = true; 198 try { 199 return resolve(s); 200 } finally { 201 scriptStyleRefuse = false; 202 } 203 } 204 205 /** 206 * Compiles the given template string into a reusable {@link VarTemplate}. 207 * 208 * <p> 209 * Equivalent to {@link VarResolver#compile(String) this.context.compile(input)}; provided on 210 * the session for symmetry with {@link #resolve(String)}. The returned template is bound to 211 * the underlying {@link VarResolver}, not to this session — see the {@link VarTemplate} 212 * class javadoc for the compile-binding contract. 213 * 214 * @param input The template string. May be {@code null}. 215 * @return A new {@link VarTemplate} bound to the underlying resolver. 216 */ 217 public VarTemplate compile(String input) { 218 return context.compile(input); 219 } 220 221 /** 222 * Returns a session-bound {@link Supplier} that re-resolves the given template against 223 * <i>this</i> session each time {@link Supplier#get()} is called. 224 * 225 * <p> 226 * Unlike {@link VarResolver#resolveSupplier(String)} — which opens a fresh session per 227 * {@code .get()} and is threadsafe to share — this Supplier captures the existing session 228 * and reuses it. <b>It is therefore not threadsafe</b>; it inherits the standard 229 * "{@link VarResolverSession} is not guaranteed to be thread safe" contract. 230 * 231 * <p> 232 * Use this variant only when the caller owns the session lifecycle and the perf saving of 233 * skipping per-call session construction matters. For shared / cross-thread use cases, 234 * prefer {@link VarResolver#resolveSupplier(String)}. 235 * 236 * @param input The template string. May be {@code null}. 237 * @return A {@code Supplier<String>} bound to this session. 238 */ 239 public Supplier<String> resolveSupplier(String input) { 240 return compile(input).asSupplier(this); 241 } 242 243 /** 244 * Resolves the specified strings in the string array. 245 * 246 * @param in The string array containing variables to resolve. Must not be <jk>null</jk> or a {@link NullPointerException} is thrown, though individual elements can be <jk>null</jk>. 247 * @return An array with resolved strings. Elements are <jk>null</jk> where the corresponding input element was <jk>null</jk>. 248 */ 249 public String[] resolve(String[] in) { 250 var out = new String[in.length]; 251 for (var i = 0; i < in.length; i++) 252 out[i] = resolve(in[i]); 253 return out; 254 } 255 256 /** 257 * Convenience method for resolving variables in arbitrary objects. 258 * 259 * <p> 260 * Supports resolving variables in the following object types: 261 * <ul> 262 * <li>{@link CharSequence} 263 * <li>Arrays containing values of type {@link CharSequence}. 264 * <li>Collections containing values of type {@link CharSequence}. 265 * <br>Collection class must have a no-arg constructor. 266 * <li>Maps containing values of type {@link CharSequence}. 267 * <br>Map class must have a no-arg constructor. 268 * </ul> 269 * 270 * @param <T> The value type. 271 * @param o The object. Can be <jk>null</jk> (returns <jk>null</jk>). 272 * @return The same object if no resolution was needed, otherwise a new object or data structure if resolution was 273 * needed. Returns <jk>null</jk> if the input was <jk>null</jk>. 274 */ 275 @SuppressWarnings({ 276 "java:S3776", // Cognitive complexity acceptable for recursive resolution logic 277 "rawtypes", // Raw types necessary for generic resolution 278 "unchecked" // Type erasure requires unchecked operations 279 }) 280 public <T> T resolve(T o) { 281 if (o == null) 282 return null; 283 if (o instanceof CharSequence o2) 284 return (T)resolve(o2.toString()); 285 if (isArray(o)) { 286 if (! containsVars(o)) 287 return o; 288 var o2 = Array.newInstance(o.getClass().getComponentType(), Array.getLength(o)); 289 for (var i = 0; i < Array.getLength(o); i++) 290 Array.set(o2, i, resolve(Array.get(o, i))); 291 return (T)o2; 292 } 293 if (o instanceof Set o2) { 294 try { 295 if (! containsVars(o2)) 296 return o; 297 Set o3 = info(o).getDeclaredConstructor(x -> x.isPublic() && x.getParameterCount() == 0).map(ci -> safe(() -> (Set)ci.inner().newInstance())).orElseGet(LinkedHashSet::new); 298 Set o4 = o3; 299 o2.forEach(x -> o4.add(resolve(x))); 300 return (T)o3; 301 } catch (VarResolverException e) { 302 throw e; 303 } catch (Exception e) { 304 throw new VarResolverException(e, "Problem occurred resolving set."); 305 } 306 } 307 if (o instanceof List o2) { 308 try { 309 if (! containsVars(o2)) 310 return o; 311 List o3 = info(o).getDeclaredConstructor(x -> x.isPublic() && x.getParameterCount() == 0).map(ci -> safe(() -> (List)ci.inner().newInstance())).orElseGet(() -> list()); 312 List o4 = o3; 313 o2.forEach(x -> o4.add(resolve(x))); 314 return (T)o3; 315 } catch (VarResolverException e) { 316 throw e; 317 } catch (Exception e) { 318 throw new VarResolverException(e, "Problem occurred resolving collection."); 319 } 320 } 321 if (o instanceof Map o2) { 322 try { 323 if (! containsVars(o2)) 324 return o; 325 Map o3 = info(o).getDeclaredConstructor(x -> x.isPublic() && x.getParameterCount() == 0).map(ci -> safe(() -> (Map)ci.inner().newInstance())).orElseGet(LinkedHashMap::new); 326 Map o4 = o3; 327 o2.forEach((k, v) -> o4.put(k, resolve(v))); 328 return (T)o3; 329 } catch (VarResolverException e) { 330 throw e; 331 } catch (Exception e) { 332 throw new VarResolverException(e, "Problem occurred resolving map."); 333 } 334 } 335 return o; 336 } 337 338 /** 339 * Resolves variables in the specified string and sends the output to the specified writer. 340 * 341 * <p> 342 * More efficient than first parsing to a string and then serializing to the writer since this method doesn't need 343 * to construct a large string. 344 * 345 * @param s The string to resolve variables in. Can be <jk>null</jk>, in which case the writer is left unchanged. 346 * @param out The writer to write to. Must not be <jk>null</jk>. 347 * @return The same writer. 348 * @throws IOException Thrown by underlying stream. 349 */ 350 public Writer resolveTo(String s, Writer out) throws IOException { 351 if (s == null) 352 return out; 353 if (s.isEmpty()) { 354 return out; 355 } 356 return context.compile(s).resolveTo(this, out); 357 } 358 359 protected FluentMap<String,Object> properties() { 360 // @formatter:off 361 return filteredBeanPropertyMap() 362 .a("context.beanStore", this.context.beanStore) 363 .a("var", this.context.getVarMap().keySet()) 364 .a("session.beanStore", beanStore); 365 // @formatter:on 366 } 367 368 @Override /* Overridden from Object */ 369 public String toString() { 370 return r(properties()); 371 } 372 373 /** 374 * Returns the {@link Var} with the specified name. 375 * 376 * @param name The var name (e.g. <js>"S"</js>). Must not be <jk>null</jk>. 377 * @return The {@link Var} instance, or <jk>null</jk> if no <c>Var</c> is associated with the specified name. 378 */ 379 protected Var getVar(String name) { 380 Var v = this.context.getVarMap().get(name); 381 return nn(v) && v.canResolve(this) ? v : null; 382 } 383 384 /** 385 * Returns <jk>true</jk> if this session is resolving HTML-doc chrome and substitutions should be XML-escaped. 386 * 387 * @return <jk>true</jk> if HTML-doc chrome encoding is enabled. 388 */ 389 boolean isHtmlDocEncode() { 390 return htmlDocEncode; 391 } 392 393 /** 394 * Returns <jk>true</jk> if {@code varName} must be left unresolved in script/style chrome. 395 * 396 * @param varName The SVL var prefix (e.g. <js>"RQ"</js>). 397 * @return <jk>true</jk> if the var should fall through like an unknown var. 398 */ 399 boolean isScriptStyleRefused(String varName) { 400 return scriptStyleRefuse && SCRIPT_STYLE_REFUSED_VARS.contains(varName); 401 } 402 403 /** 404 * XML-escapes an SVL substitution when HTML-doc chrome encoding is enabled and the var is not passthrough. 405 * 406 * @param varName The SVL var prefix. 407 * @param replacement The resolved substitution text. 408 * @return The possibly escaped replacement. 409 */ 410 String applyHtmlDocChromeEncoding(String varName, String replacement) { 411 if (! htmlDocEncode || replacement == null || HTML_DOC_PASSTHROUGH_VARS.contains(varName)) 412 return replacement; 413 return StringUtils.escapeXml(replacement); 414 } 415}