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