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}