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