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;
18  
19  import static org.apache.juneau.commons.utils.AssertionUtils.*;
20  import static org.apache.juneau.commons.utils.CollectionUtils.*;
21  import static org.apache.juneau.commons.utils.Utils.*;
22  
23  import java.text.*;
24  import java.util.*;
25  import java.util.function.*;
26  
27  import org.apache.juneau.collections.*;
28  import org.apache.juneau.commons.collections.FluentMap;
29  import org.apache.juneau.commons.utils.*;
30  
31  /**
32   * ContextSession that lives for the duration of a single use of {@link BeanTraverseContext}.
33   *
34   * <p>
35   * Used by serializers and other classes that traverse POJOs for the following purposes:
36   * <ul class='spaced-list'>
37   * 	<li>
38   * 		Keeping track of how deep it is in a model for indentation purposes.
39   * 	<li>
40   * 		Ensuring infinite loops don't occur by setting a limit on how deep to traverse a model.
41   * 	<li>
42   * 		Ensuring infinite loops don't occur from loops in the model (when detectRecursions is enabled.
43   * </ul>
44   *
45   * <h5 class='section'>Notes:</h5><ul>
46   * 	<li class='warn'>This class is not thread safe and is typically discarded after one use.
47   * </ul>
48   *
49   */
50  public class BeanTraverseSession extends BeanSession {
51  	/**
52  	 * Builder class.
53  	 */
54  	public static abstract class Builder extends BeanSession.Builder {
55  
56  		private BeanTraverseContext ctx;
57  		private int initialDepth;
58  
59  		/**
60  		 * Constructor
61  		 *
62  		 * @param ctx The context creating this session.
63  		 * 	<br>Cannot be <jk>null</jk>.
64  		 */
65  		protected Builder(BeanTraverseContext ctx) {
66  			super(assertArgNotNull("ctx", ctx).getBeanContext());
67  			this.ctx = ctx;
68  			initialDepth = ctx.getInitialDepth();
69  		}
70  
71  		@Override /* Overridden from Builder */
72  		public <T> Builder apply(Class<T> type, Consumer<T> apply) {
73  			super.apply(type, apply);
74  			return this;
75  		}
76  
77  		@Override /* Overridden from Builder */
78  		public Builder debug(Boolean value) {
79  			super.debug(value);
80  			return this;
81  		}
82  
83  		@Override /* Overridden from Builder */
84  		public Builder locale(Locale value) {
85  			super.locale(value);
86  			return this;
87  		}
88  
89  
90  		@Override /* Overridden from Builder */
91  		public Builder mediaType(MediaType value) {
92  			super.mediaType(value);
93  			return this;
94  		}
95  
96  		@Override /* Overridden from Builder */
97  		public Builder mediaTypeDefault(MediaType value) {
98  			super.mediaTypeDefault(value);
99  			return this;
100 		}
101 
102 		@Override /* Overridden from Builder */
103 		public Builder properties(Map<String,Object> value) {
104 			super.properties(value);
105 			return this;
106 		}
107 
108 		@Override /* Overridden from Builder */
109 		public Builder property(String key, Object value) {
110 			super.property(key, value);
111 			return this;
112 		}
113 
114 		@Override /* Overridden from Builder */
115 		public Builder timeZone(TimeZone value) {
116 			super.timeZone(value);
117 			return this;
118 		}
119 
120 		@Override /* Overridden from Builder */
121 		public Builder timeZoneDefault(TimeZone value) {
122 			super.timeZoneDefault(value);
123 			return this;
124 		}
125 
126 		@Override /* Overridden from Builder */
127 		public Builder unmodifiable() {
128 			super.unmodifiable();
129 			return this;
130 		}
131 	}
132 
133 	private class StackElement {
134 		final int depth;
135 		final String name;
136 		final Object o;
137 		final ClassMeta<?> aType;
138 
139 		StackElement(int depth, String name, Object o, ClassMeta<?> aType) {
140 			this.depth = depth;
141 			this.name = name;
142 			this.o = o;
143 			this.aType = aType;
144 		}
145 
146 		String toString(boolean simple) {
147 			var sb = new StringBuilder().append('[').append(depth).append(']').append(' ');
148 			sb.append(e(name) ? "<noname>" : name).append(':');
149 			sb.append(aType.toString(simple));
150 			if (aType != aType.getSerializedClassMeta(BeanTraverseSession.this))
151 				sb.append('/').append(aType.getSerializedClassMeta(BeanTraverseSession.this).toString(simple));
152 			return sb.toString();
153 		}
154 	}
155 
156 	private final BeanTraverseContext ctx;
157 	private final LinkedList<StackElement> stack = new LinkedList<>();              // Contains the current objects in the current branch of the model.
158 	private final Map<Object,Object> set;                                           // Contains the current objects in the current branch of the model.
159 	private BeanPropertyMeta currentProperty;
160 	private ClassMeta<?> currentClass;
161 	private boolean isBottom;                                                       // If 'true', then we're at a leaf in the model (i.e. a String, Number, Boolean, or null).
162 	/** The current indentation depth into the model. */
163 	public int indent;
164 	private int depth;
165 
166 	/**
167 	 * Constructor.
168 	 *
169 	 * @param builder The builder for this object.
170 	 */
171 	protected BeanTraverseSession(Builder builder) {
172 		super(builder);
173 		ctx = builder.ctx;
174 		indent = builder.initialDepth;
175 		if (isDetectRecursions() || isDebug()) {
176 			set = new IdentityHashMap<>();
177 		} else {
178 			set = mape();
179 		}
180 	}
181 
182 	/**
183 	 * Initial depth.
184 	 *
185 	 * @see BeanTraverseContext.Builder#initialDepth(int)
186 	 * @return
187 	 * 	The initial indentation level at the root.
188 	 */
189 	public final int getInitialDepth() { return ctx.getInitialDepth(); }
190 
191 	/**
192 	 * Returns information used to determine at what location in the parse a failure occurred.
193 	 *
194 	 * @return A map, typically containing something like <c>{line:123,column:456,currentProperty:"foobar"}</c>
195 	 */
196 	public final JsonMap getLastLocation() {
197 		Predicate<Object> nn = Utils::nn;
198 		Predicate<Collection<?>> nec = Utils::ne;
199 		// @formatter:off
200 		return JsonMap
201 			.create()
202 			.appendIf(nn, "currentClass", currentClass)
203 			.appendIf(nn, "currentProperty", currentProperty)
204 			.appendIf(nec, "stack", stack);
205 		// @formatter:on
206 	}
207 
208 	/**
209 	 * Max traversal depth.
210 	 *
211 	 * @see BeanTraverseContext.Builder#maxDepth(int)
212 	 * @return
213 	 * 	The depth at which traversal is aborted if depth is reached in the POJO tree.
214 	 *	<br>If this depth is exceeded, an exception is thrown.
215 	 */
216 	public final int getMaxDepth() { return ctx.getMaxDepth(); }
217 
218 	/**
219 	 * Automatically detect POJO recursions.
220 	 *
221 	 * @see BeanTraverseContext.Builder#detectRecursions()
222 	 * @return
223 	 * 	<jk>true</jk> if recursions should be checked for during traversal.
224 	 */
225 	public final boolean isDetectRecursions() { return ctx.isDetectRecursions(); }
226 
227 	/**
228 	 * Ignore recursion errors.
229 	 *
230 	 * @see BeanTraverseContext.Builder#ignoreRecursions()
231 	 * @return
232 	 * 	<jk>true</jk> if when we encounter the same object when traversing a tree, we set the value to <jk>null</jk>.
233 	 * 	<br>Otherwise, a {@link BeanRecursionException} is thrown with the message <js>"Recursion occurred, stack=..."</js>.
234 	 */
235 	public final boolean isIgnoreRecursions() { return ctx.isIgnoreRecursions(); }
236 
237 	/**
238 	 * Returns the inner type of an {@link Optional}.
239 	 *
240 	 * @param cm The meta to check.
241 	 * @return The inner type of an {@link Optional}.
242 	 */
243 	protected final ClassMeta<?> getOptionalType(ClassMeta<?> cm) {
244 		if (cm.isOptional())
245 			return getOptionalType(cm.getElementType());
246 		return cm;
247 	}
248 
249 	/**
250 	 * If the specified object is an {@link Optional}, returns the inner object.
251 	 *
252 	 * @param o The object to check.
253 	 * @return The inner object if it's an {@link Optional}, <jk>null</jk> if it's <jk>null</jk>, or else the same object.
254 	 */
255 	protected final Object getOptionalValue(Object o) {
256 		if (o == null)
257 			return null;
258 		if (o instanceof Optional<?> o2)
259 			return getOptionalValue(o2.orElse(null));
260 		return o;
261 	}
262 
263 	/**
264 	 * Returns the current stack trace.
265 	 *
266 	 * @param full
267 	 * 	If <jk>true</jk>, returns a full stack trace.
268 	 * @return The current stack trace.
269 	 */
270 	protected String getStack(boolean full) {
271 		var sb = new StringBuilder();
272 		stack.forEach(x -> {
273 			if (full) {
274 				sb.append("\n\t");
275 				for (var i = 1; i < x.depth; i++)
276 					sb.append("  ");
277 				if (x.depth > 0)
278 					sb.append("->");
279 				sb.append(x.toString(false));
280 			} else {
281 				sb.append(" > ").append(x.toString(true));
282 			}
283 		});
284 		return sb.toString();
285 	}
286 
287 	/**
288 	 * Same as {@link ClassMeta#isOptional()} but gracefully handles a null {@link ClassMeta}.
289 	 *
290 	 * @param cm The meta to check.
291 	 * @return <jk>true</jk> if the specified meta is an {@link Optional}.
292 	 */
293 	protected final static boolean isOptional(ClassMeta<?> cm) {
294 		return (nn(cm) && cm.isOptional());
295 	}
296 
297 	/**
298 	 * Returns <jk>true</jk> if we're processing the root node.
299 	 *
300 	 * <p>
301 	 * Must be called after {@link #push(String, Object, ClassMeta)} and before {@link #pop()}.
302 	 *
303 	 * @return <jk>true</jk> if we're processing the root node.
304 	 */
305 	protected final boolean isRoot() { return depth == 1; }
306 
307 	/**
308 	 * Logs a warning message.
309 	 *
310 	 * @param t The throwable that was thrown (if there was one).
311 	 * @param msg The warning message.
312 	 * @param args Optional {@link MessageFormat}-style arguments.
313 	 */
314 	protected void onError(Throwable t, String msg, Object...args) {
315 		super.addWarning(msg, args);
316 	}
317 
318 	/**
319 	 * Pop an object off the stack.
320 	 */
321 	protected final void pop() {
322 		indent--;
323 		depth--;
324 		if ((isDetectRecursions() || isDebug()) && ! isBottom) {
325 			Object o = stack.removeLast().o;
326 			Object o2 = set.remove(o);
327 			if (o2 == null)
328 				onError(null, "Couldn't remove object of type ''{0}'' on attribute ''{1}'' from object stack.", cn(o), stack);
329 		}
330 		isBottom = false;
331 	}
332 
333 	@Override /* Overridden from BeanSession */
334 	protected FluentMap<String,Object> properties() {
335 		return super.properties()
336 			.a("indent", indent)
337 			.a("depth", depth);
338 	}
339 
340 	/**
341 	 * Push the specified object onto the stack.
342 	 *
343 	 * @param attrName The attribute name.
344 	 * @param o The current object being traversed.
345 	 * @param eType The expected class type.
346 	 * @return
347 	 * 	The {@link ClassMeta} of the object so that <c>instanceof</c> operations only need to be performed
348 	 * 	once (since they can be expensive).
349 	 * @throws BeanRecursionException If recursion occurred.
350 	 */
351 	protected final ClassMeta<?> push(String attrName, Object o, ClassMeta<?> eType) throws BeanRecursionException {
352 		indent++;
353 		depth++;
354 		isBottom = true;
355 		if (o == null)
356 			return null;
357 		var c = o.getClass();
358 		var cm = (nn(eType) && c == eType.inner()) ? eType : ((o instanceof ClassMeta) ? (ClassMeta<?>)o : getClassMeta(c));
359 		if (cm.isCharSequence() || cm.isNumber() || cm.isBoolean())
360 			return cm;
361 		if (depth > getMaxDepth())
362 			return null;
363 		if (isDetectRecursions() || isDebug()) {
364 			if (willRecurse(attrName, o, cm))
365 				return null;
366 			isBottom = false;
367 			stack.add(new StackElement(stack.size(), attrName, o, cm));
368 			set.put(o, o);
369 		}
370 		return cm;
371 	}
372 
373 	/**
374 	 * Sets the current class being traversed for proper error messages.
375 	 *
376 	 * @param currentClass The current class being traversed.
377 	 */
378 	protected final void setCurrentClass(ClassMeta<?> currentClass) { this.currentClass = currentClass; }
379 
380 	/**
381 	 * Sets the current bean property being traversed for proper error messages.
382 	 *
383 	 * @param currentProperty The current property being traversed.
384 	 */
385 	protected final void setCurrentProperty(BeanPropertyMeta currentProperty) { this.currentProperty = currentProperty; }
386 
387 	/**
388 	 * Returns <jk>true</jk> if we're about to exceed the max depth for the document.
389 	 *
390 	 * @return <jk>true</jk> if we're about to exceed the max depth for the document.
391 	 */
392 	protected final boolean willExceedDepth() {
393 		return (depth >= getMaxDepth());
394 	}
395 
396 	/**
397 	 * Returns <jk>true</jk> if {@link BeanTraverseContext.Builder#detectRecursions()} is enabled, and the specified
398 	 * object is already higher up in the traversal chain.
399 	 *
400 	 * @param attrName The bean property attribute name, or some other identifier.
401 	 * @param o The object to check for recursion.
402 	 * @param cm The metadata on the object class.
403 	 * @return <jk>true</jk> if recursion detected.
404 	 * @throws BeanRecursionException If recursion occurred.
405 	 */
406 	protected final boolean willRecurse(String attrName, Object o, ClassMeta<?> cm) throws BeanRecursionException {
407 		if (! (isDetectRecursions() || isDebug()) || ! set.containsKey(o))
408 			return false;
409 		if (isIgnoreRecursions() && ! isDebug())
410 			return true;
411 
412 		stack.add(new StackElement(stack.size(), attrName, o, cm));
413 		throw new BeanRecursionException("Recursion occurred, stack={0}", getStack(true));
414 	}
415 }