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