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}