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