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}