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