View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.juneau.svl;
18  
19  import static org.apache.juneau.commons.lang.StateEnum.*;
20  import static org.apache.juneau.commons.reflect.ReflectionUtils.*;
21  import static org.apache.juneau.commons.utils.CollectionUtils.*;
22  import static org.apache.juneau.commons.utils.StringUtils.*;
23  import static org.apache.juneau.commons.utils.ThrowableUtils.*;
24  import static org.apache.juneau.commons.utils.Utils.*;
25  
26  import java.io.*;
27  import java.lang.reflect.*;
28  import java.util.*;
29  
30  import org.apache.juneau.commons.collections.*;
31  import org.apache.juneau.commons.lang.*;
32  import org.apache.juneau.cp.*;
33  
34  /**
35   * A var resolver session that combines a {@link VarResolver} with one or more session objects.
36   *
37   * <p>
38   * Instances of this class are considered light-weight and fast to construct, use, and discard.
39   *
40   * <p>
41   * This class contains the workhorse code for var resolution.
42   *
43   * <p>
44   * Instances of this class are created through the {@link VarResolver#createSession()} and
45   * {@link VarResolver#createSession(BeanStore)} methods.
46   *
47   * <h5 class='section'>Notes:</h5><ul>
48   * 	<li class='warn'>This class is not guaranteed to be thread safe.
49   * </ul>
50   *
51   * <h5 class='section'>See Also:</h5><ul>
52   * 	<li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/SimpleVariableLanguageBasics">Simple Variable Language Basics</a>
53   * </ul>
54   */
55  @SuppressWarnings("resource")
56  public class VarResolverSession {
57  
58  	private static final AsciiSet AS1 = AsciiSet.of("\\{"), AS2 = AsciiSet.of("\\${}");
59  
60  	private static boolean containsVars(Collection<?> c) {
61  		var f = Flag.create();
62  		c.forEach(x -> {
63  			if (x instanceof CharSequence && x.toString().contains("$"))
64  				f.set();
65  		});
66  		return f.isSet();
67  	}
68  
69  	private static boolean containsVars(Map<?,?> m) {
70  		var f = Flag.create();
71  		m.forEach((k, v) -> {
72  			if (v instanceof CharSequence && v.toString().contains("$"))
73  				f.set();
74  		});
75  		return f.isSet();
76  	}
77  
78  	private static boolean containsVars(Object array) {
79  		for (var i = 0; i < Array.getLength(array); i++) {
80  			var o = Array.get(array, i);
81  			if (o instanceof CharSequence && o.toString().contains("$"))
82  				return true;
83  		}
84  		return false;
85  	}
86  
87  	/*
88  	 * Checks to see if string is of the simple form "$X{...}" with no embedded variables.
89  	 * This is a common case, and we can avoid using StringWriters.
90  	 */
91  	private static boolean isSimpleVar(String s) {
92  		// S1: Not in variable, looking for $
93  		// S2: Found $, Looking for {
94  		// S3: Found {, Looking for }
95  		// S4: Found }
96  
97  		int length = s.length();
98  		var state = S1;
99  		for (var i = 0; i < length; i++) {
100 			var c = s.charAt(i);
101 			if (state == S1) {
102 				if (c == '$') {
103 					state = S2;
104 				} else {
105 					return false;
106 				}
107 			} else if (state == S2) {
108 				if (c == '{') {
109 					state = S3;
110 				} else if (c < 'A' || c > 'z' || (c > 'Z' && c < 'a')) {   // False trigger "$X "
111 					return false;
112 				}
113 			} else if (state == S3) {
114 				if (c == '}')
115 					state = S4;
116 				else if (c == '{' || c == '$')
117 					return false;
118 			} else if (state == S4) {
119 				return false;
120 			}
121 		}
122 		return state == S4;
123 	}
124 
125 	private final VarResolver context;
126 
127 	private final BeanStore beanStore;
128 
129 	/**
130 	 * Constructor.
131 	 *
132 	 * @param context
133 	 * 	The {@link VarResolver} context object that contains the {@link Var Vars} and context objects associated with
134 	 * 	that resolver.
135 	 * @param beanStore The bean store to use for resolving beans needed by vars.
136 	 *
137 	 */
138 	public VarResolverSession(VarResolver context, BeanStore beanStore) {
139 		this.context = context;
140 		this.beanStore = BeanStore.of(beanStore);
141 	}
142 
143 	/**
144 	 * Adds a bean to this session.
145 	 *
146 	 * @param <T> The bean type.
147 	 * @param c The bean type.
148 	 * @param value The bean.
149 	 * @return This object.
150 	 */
151 	public <T> VarResolverSession bean(Class<T> c, T value) {
152 		beanStore.addBean(c, value);
153 		return this;
154 	}
155 
156 	/**
157 	 * Returns the bean from the registered bean store.
158 	 *
159 	 * @param <T> The value type.
160 	 * @param c The bean type.
161 	 * @return
162 	 * 	The bean.
163 	 * 	<br>Never <jk>null</jk>.
164 	 */
165 	public <T> Optional<T> getBean(Class<T> c) {
166 		Optional<T> t = beanStore.getBean(c);
167 		if (! t.isPresent())
168 			t = context.beanStore.getBean(c);
169 		return t;
170 	}
171 
172 	/**
173 	 * Resolve all variables in the specified string.
174 	 *
175 	 * @param s
176 	 * 	The string to resolve variables in.
177 	 * @return
178 	 * 	The new string with all variables resolved, or the same string if no variables were found.
179 	 * 	<br>Returns <jk>null</jk> if the input was <jk>null</jk>.
180 	 */
181 	public String resolve(String s) {
182 
183 		if (s == null || s.isEmpty() || (s.indexOf('$') == -1 && s.indexOf('\\') == -1))
184 			return s;
185 
186 		// Special case where value consists of a single variable with no embedded variables (e.g. "$X{...}").
187 		// This is a common case, so we want an optimized solution that doesn't involve string builders.
188 		if (isSimpleVar(s)) {
189 			String var = s.substring(1, s.indexOf('{'));
190 			String val = s.substring(s.indexOf('{') + 1, s.length() - 1);
191 			Var v = getVar(var);
192 			if (nn(v)) {
193 				try {
194 					if (v.streamed) {
195 						var sw = new StringWriter();
196 						v.resolveTo(this, sw, val);
197 						return sw.toString();
198 					}
199 					s = v.doResolve(this, val);
200 					if (s == null)
201 						s = "";
202 					return (v.allowRecurse() ? resolve(s) : s);
203 				} catch (VarResolverException e) {
204 					throw e;
205 				} catch (Exception e) {
206 					throw new VarResolverException(e, "Problem occurred resolving variable ''{0}'' in string ''{1}''", var, s);
207 				}
208 			}
209 			return s;
210 		}
211 
212 		try {
213 			return resolveTo(s, new StringWriter()).toString();
214 		} catch (IOException e) {
215 			throw toRex(e); // Never happens.
216 		}
217 	}
218 
219 	/**
220 	 * Resolves the specified strings in the string array.
221 	 *
222 	 * @param in The string array containing variables to resolve.
223 	 * @return An array with resolved strings.
224 	 */
225 	public String[] resolve(String[] in) {
226 		var out = new String[in.length];
227 		for (var i = 0; i < in.length; i++)
228 			out[i] = resolve(in[i]);
229 		return out;
230 	}
231 
232 	/**
233 	 * Convenience method for resolving variables in arbitrary objects.
234 	 *
235 	 * <p>
236 	 * Supports resolving variables in the following object types:
237 	 * <ul>
238 	 * 	<li>{@link CharSequence}
239 	 * 	<li>Arrays containing values of type {@link CharSequence}.
240 	 * 	<li>Collections containing values of type {@link CharSequence}.
241 	 * 		<br>Collection class must have a no-arg constructor.
242 	 * 	<li>Maps containing values of type {@link CharSequence}.
243 	 * 		<br>Map class must have a no-arg constructor.
244 	 * </ul>
245 	 *
246 	 * @param <T> The value type.
247 	 * @param o The object.
248 	 * @return The same object if no resolution was needed, otherwise a new object or data structure if resolution was
249 	 * needed.
250 	 */
251 	@SuppressWarnings({ "rawtypes", "unchecked" })
252 	public <T> T resolve(T o) {
253 		if (o == null)
254 			return null;
255 		if (o instanceof CharSequence o2)
256 			return (T)resolve(o2.toString());
257 		if (isArray(o)) {
258 			if (! containsVars(o))
259 				return o;
260 			var o2 = Array.newInstance(o.getClass().getComponentType(), Array.getLength(o));
261 			for (var i = 0; i < Array.getLength(o); i++)
262 				Array.set(o2, i, resolve(Array.get(o, i)));
263 			return (T)o2;
264 		}
265 		if (o instanceof Set o2) {
266 			try {
267 				if (! containsVars(o2))
268 					return o;
269 				Set o3 = info(o).getDeclaredConstructor(x -> x.isPublic() && x.getParameterCount() == 0).map(ci -> safe(() -> (Set)ci.inner().newInstance())).orElseGet(LinkedHashSet::new);
270 				Set o4 = o3;
271 				o2.forEach(x -> o4.add(resolve(x)));
272 				return (T)o3;
273 			} catch (VarResolverException e) {
274 				throw e;
275 			} catch (Exception e) {
276 				throw new VarResolverException(e, "Problem occurred resolving set.");
277 			}
278 		}
279 		if (o instanceof List o2) {
280 			try {
281 				if (! containsVars(o2))
282 					return o;
283 				List o3 = info(o).getDeclaredConstructor(x -> x.isPublic() && x.getParameterCount() == 0).map(ci -> safe(() -> (List)ci.inner().newInstance())).orElseGet(() -> list());
284 				List o4 = o3;
285 				o2.forEach(x -> o4.add(resolve(x)));
286 				return (T)o3;
287 			} catch (VarResolverException e) {
288 				throw e;
289 			} catch (Exception e) {
290 				throw new VarResolverException(e, "Problem occurred resolving collection.");
291 			}
292 		}
293 		if (o instanceof Map o2) {
294 			try {
295 				if (! containsVars(o2))
296 					return o;
297 				Map o3 = info(o).getDeclaredConstructor(x -> x.isPublic() && x.getParameterCount() == 0).map(ci -> safe(() -> (Map)ci.inner().newInstance())).orElseGet(LinkedHashMap::new);
298 				Map o4 = o3;
299 				o2.forEach((k, v) -> o4.put(k, resolve(v)));
300 				return (T)o3;
301 			} catch (VarResolverException e) {
302 				throw e;
303 			} catch (Exception e) {
304 				throw new VarResolverException(e, "Problem occurred resolving map.");
305 			}
306 		}
307 		return o;
308 	}
309 
310 	/**
311 	 * Resolves variables in the specified string and sends the output to the specified writer.
312 	 *
313 	 * <p>
314 	 * More efficient than first parsing to a string and then serializing to the writer since this method doesn't need
315 	 * to construct a large string.
316 	 *
317 	 * @param s The string to resolve variables in.
318 	 * @param out The writer to write to.
319 	 * @return The same writer.
320 	 * @throws IOException Thrown by underlying stream.
321 	 */
322 	public Writer resolveTo(String s, Writer out) throws IOException {
323 
324 		// S1: Not in variable, looking for $
325 		// S2: Found $, Looking for {
326 		// S3: Found {, Looking for }
327 
328 		var state = S1;
329 		var isInEscape = false;
330 		var hasInternalVar = false;
331 		var hasInnerEscapes = false;
332 		var varType = (String)null;
333 		var varVal = (String)null;
334 		var x = 0;
335 		var x2 = 0;
336 		var depth = 0;
337 		var length = s.length();
338 		for (var i = 0; i < length; i++) {
339 			var c = s.charAt(i);
340 			if (state == S1) {
341 				if (isInEscape) {
342 					if (c == '\\' || c == '$') {
343 						out.append(c);
344 					} else {
345 						out.append('\\').append(c);
346 					}
347 					isInEscape = false;
348 				} else if (c == '\\') {
349 					isInEscape = true;
350 				} else if (c == '$') {
351 					x = i;
352 					x2 = i;
353 					state = S2;
354 				} else {
355 					out.append(c);
356 				}
357 			} else if (state == S2) {
358 				if (isInEscape) {
359 					isInEscape = false;
360 				} else if (c == '\\') {
361 					hasInnerEscapes = true;
362 					isInEscape = true;
363 				} else if (c == '{') {
364 					varType = s.substring(x + 1, i);
365 					x = i;
366 					state = S3;
367 				} else if (c < 'A' || c > 'z' || (c > 'Z' && c < 'a')) {  // False trigger "$X "
368 					if (hasInnerEscapes)
369 						out.append(unescapeChars(s.substring(x, i + 1), AS1));
370 					else
371 						out.append(s, x, i + 1);
372 					x = i + 1;
373 					state = S1;
374 					hasInnerEscapes = false;
375 				}
376 			} else if (state == S3) {
377 				if (isInEscape) {
378 					isInEscape = false;
379 				} else if (c == '\\') {
380 					isInEscape = true;
381 					hasInnerEscapes = true;
382 				} else if (c == '{') {
383 					depth++;
384 					hasInternalVar = true;
385 				} else if (c == '}') {
386 					if (depth > 0) {
387 						depth--;
388 					} else {
389 						varVal = s.substring(x + 1, i);
390 						Var r = getVar(varType);
391 						if (r == null) {
392 							if (hasInnerEscapes)
393 								out.append(unescapeChars(s.substring(x2, i + 1), AS2));
394 							else
395 								out.append(s, x2, i + 1);
396 							x = i + 1;
397 						} else {
398 							varVal = (hasInternalVar && r.allowNested() ? resolve(varVal) : varVal);
399 							try {
400 								if (r.streamed)
401 									r.resolveTo(this, out, varVal);
402 								else {
403 									String replacement = r.doResolve(this, varVal);
404 									if (replacement == null)
405 										replacement = "";
406 									// If the replacement also contains variables, replace them now.
407 									if (replacement.indexOf('$') != -1 && r.allowRecurse())
408 										replacement = resolve(replacement);
409 									out.append(replacement);
410 								}
411 							} catch (VarResolverException e) {
412 								throw e;
413 							} catch (Exception e) {
414 								throw new VarResolverException(e, "Problem occurred resolving variable ''{0}'' in string ''{1}''", varType, s);
415 							}
416 							x = i + 1;
417 						}
418 						state = S1;
419 						hasInnerEscapes = false;
420 					}
421 				}
422 			}
423 		}
424 		if (isInEscape)
425 			out.append('\\');
426 		else if (state == S2)
427 			out.append('$').append(unescapeChars(s.substring(x + 1), AS1));
428 		else if (state == S3)
429 			out.append('$').append(varType).append('{').append(unescapeChars(s.substring(x + 1), AS2));
430 		return out;
431 	}
432 
433 	protected FluentMap<String,Object> properties() {
434 		// @formatter:off
435 		return filteredBeanPropertyMap()
436 			.a("context.beanStore", this.context.beanStore)
437 			.a("var", this.context.getVarMap().keySet())
438 			.a("session.beanStore", beanStore);
439 		// @formatter:on
440 	}
441 
442 	@Override /* Overridden from Object */
443 	public String toString() {
444 		return r(properties());
445 	}
446 
447 	/**
448 	 * Returns the {@link Var} with the specified name.
449 	 *
450 	 * @param name The var name (e.g. <js>"S"</js>).
451 	 * @return The {@link Var} instance, or <jk>null</jk> if no <c>Var</c> is associated with the specified name.
452 	 */
453 	protected Var getVar(String name) {
454 		Var v = this.context.getVarMap().get(name);
455 		return nn(v) && v.canResolve(this) ? v : null;
456 	}
457 }