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;
014
015import static org.apache.juneau.internal.StringUtils.*;
016
017import java.text.*;
018import java.util.*;
019
020import org.apache.juneau.collections.*;
021
022/**
023 * Session that lives for the duration of a single use of {@link BeanTraverseContext}.
024 *
025 * <p>
026 * Used by serializers and other classes that traverse POJOs for the following purposes:
027 * <ul class='spaced-list'>
028 *    <li>
029 *       Keeping track of how deep it is in a model for indentation purposes.
030 *    <li>
031 *       Ensuring infinite loops don't occur by setting a limit on how deep to traverse a model.
032 *    <li>
033 *       Ensuring infinite loops don't occur from loops in the model (when detectRecursions is enabled.
034 * </ul>
035 *
036 * <p>
037 * This class is NOT thread safe.
038 * It is typically discarded after one-time use although it can be reused within the same thread.
039 */
040public class BeanTraverseSession extends BeanSession {
041
042   private final BeanTraverseContext ctx;
043   private final Map<Object,Object> set;                                           // Contains the current objects in the current branch of the model.
044   private final LinkedList<StackElement> stack = new LinkedList<>();              // Contains the current objects in the current branch of the model.
045
046   // Writable properties
047   private boolean isBottom;                                                       // If 'true', then we're at a leaf in the model (i.e. a String, Number, Boolean, or null).
048   private BeanPropertyMeta currentProperty;
049   private ClassMeta<?> currentClass;
050
051   /** The current indentation depth into the model. */
052   public int indent;
053
054   private int depth;
055
056
057   /**
058    * Create a new session using properties specified in the context.
059    *
060    * @param ctx
061    *    The context creating this session object.
062    *    The context contains all the configuration settings for this object.
063    *    Can be <jk>null</jk>.
064    * @param args
065    *    Runtime arguments.
066    *    These specify session-level information such as locale and URI context.
067    *    It also include session-level properties that override the properties defined on the bean and
068    *    serializer contexts.
069    */
070   protected BeanTraverseSession(BeanTraverseContext ctx, BeanSessionArgs args) {
071      super(ctx, args == null ? BeanSessionArgs.DEFAULT : args);
072      args = args == null ? BeanSessionArgs.DEFAULT : args;
073      this.ctx = ctx;
074      this.indent = getInitialDepth();
075      if (isDetectRecursions() || isDebug()) {
076         set = new IdentityHashMap<>();
077      } else {
078         set = Collections.emptyMap();
079      }
080   }
081
082   /**
083    * Sets the current bean property being traversed for proper error messages.
084    *
085    * @param currentProperty The current property being traversed.
086    */
087   protected final void setCurrentProperty(BeanPropertyMeta currentProperty) {
088      this.currentProperty = currentProperty;
089   }
090
091   /**
092    * Sets the current class being traversed for proper error messages.
093    *
094    * @param currentClass The current class being traversed.
095    */
096   protected final void setCurrentClass(ClassMeta<?> currentClass) {
097      this.currentClass = currentClass;
098   }
099
100   /**
101    * Push the specified object onto the stack.
102    *
103    * @param attrName The attribute name.
104    * @param o The current object being traversed.
105    * @param eType The expected class type.
106    * @return
107    *    The {@link ClassMeta} of the object so that <c>instanceof</c> operations only need to be performed
108    *    once (since they can be expensive).
109    * @throws BeanRecursionException If recursion occurred.
110    */
111   protected final ClassMeta<?> push(String attrName, Object o, ClassMeta<?> eType) throws BeanRecursionException {
112      indent++;
113      depth++;
114      isBottom = true;
115      if (o == null)
116         return null;
117      Class<?> c = o.getClass();
118      ClassMeta<?> cm = (eType != null && c == eType.getInnerClass()) ? eType : ((o instanceof ClassMeta) ? (ClassMeta<?>)o : getClassMeta(c));
119      if (cm.isCharSequence() || cm.isNumber() || cm.isBoolean())
120         return cm;
121      if (depth > getMaxDepth())
122         return null;
123      if (isDetectRecursions() || isDebug()) {
124         if (willRecurse(attrName, o, cm))
125            return null;
126         isBottom = false;
127         stack.add(new StackElement(stack.size(), attrName, o, cm));
128         set.put(o, o);
129      }
130      return cm;
131   }
132
133   /**
134    * Returns <jk>true</jk> if we're processing the root node.
135    *
136    * <p>
137    * Must be called after {@link #push(String, Object, ClassMeta)} and before {@link #pop()}.
138    *
139    * @return <jk>true</jk> if we're processing the root node.
140    */
141   protected final boolean isRoot() {
142      return depth == 1;
143   }
144
145   /**
146    * Returns <jk>true</jk> if {@link BeanTraverseContext#BEANTRAVERSE_detectRecursions} is enabled, and the specified
147    * object is already higher up in the traversal chain.
148    *
149    * @param attrName The bean property attribute name, or some other identifier.
150    * @param o The object to check for recursion.
151    * @param cm The metadata on the object class.
152    * @return <jk>true</jk> if recursion detected.
153    * @throws BeanRecursionException If recursion occurred.
154    */
155   protected final boolean willRecurse(String attrName, Object o, ClassMeta<?> cm) throws BeanRecursionException {
156      if (! (isDetectRecursions() || isDebug()))
157         return false;
158      if (! set.containsKey(o))
159         return false;
160      if (isIgnoreRecursions() && ! isDebug())
161         return true;
162
163      stack.add(new StackElement(stack.size(), attrName, o, cm));
164      throw new BeanRecursionException("Recursion occurred, stack={0}", getStack(true));
165   }
166
167   /**
168    * Returns <jk>true</jk> if we're about to exceed the max depth for the document.
169    *
170    * @return <jk>true</jk> if we're about to exceed the max depth for the document.
171    */
172   protected final boolean willExceedDepth() {
173      return (depth >= getMaxDepth());
174   }
175
176   /**
177    * Pop an object off the stack.
178    */
179   protected final void pop() {
180      indent--;
181      depth--;
182      if ((isDetectRecursions() || isDebug()) && ! isBottom)  {
183         Object o = stack.removeLast().o;
184         Object o2 = set.remove(o);
185         if (o2 == null)
186            onError(null, "Couldn't remove object of type ''{0}'' on attribute ''{1}'' from object stack.",
187               o.getClass().getName(), stack);
188      }
189      isBottom = false;
190   }
191
192   /**
193    * Same as {@link ClassMeta#isOptional()} but gracefully handles a null {@link ClassMeta}.
194    *
195    * @param cm The meta to check.
196    * @return <jk>true</jk> if the specified meta is an {@link Optional}.
197    */
198   protected final boolean isOptional(ClassMeta<?> cm) {
199      return (cm != null && cm.isOptional());
200   }
201
202   /**
203    * Returns the inner type of an {@link Optional}.
204    *
205    * @param cm The meta to check.
206    * @return The inner type of an {@link Optional}.
207    */
208   protected final ClassMeta<?> getOptionalType(ClassMeta<?> cm) {
209      if (cm.isOptional())
210         return getOptionalType(cm.getElementType());
211      return cm;
212   }
213
214   /**
215    * If the specified object is an {@link Optional}, returns the inner object.
216    *
217    * @param o The object to check.
218    * @return The inner object if it's an {@link Optional}, <jk>null</jk> if it's <jk>null</jk>, or else the same object.
219    */
220   protected final Object getOptionalValue(Object o) {
221      if (o == null)
222         return null;
223      if (o instanceof Optional)
224         return getOptionalValue(((Optional<?>)o).orElse(null));
225      return o;
226   }
227
228   /**
229    * Logs a warning message.
230    *
231    * @param t The throwable that was thrown (if there was one).
232    * @param msg The warning message.
233    * @param args Optional {@link MessageFormat}-style arguments.
234    */
235   protected void onError(Throwable t, String msg, Object... args) {
236      super.addWarning(msg, args);
237   }
238
239   private final class StackElement {
240      final int depth;
241      final String name;
242      final Object o;
243      final ClassMeta<?> aType;
244
245      StackElement(int depth, String name, Object o, ClassMeta<?> aType) {
246         this.depth = depth;
247         this.name = name;
248         this.o = o;
249         this.aType = aType;
250      }
251
252      String toString(boolean simple) {
253         StringBuilder sb = new StringBuilder().append('[').append(depth).append(']').append(' ');
254         sb.append(isEmpty(name) ? "<noname>" : name).append(':');
255         sb.append(aType.toString(simple));
256         if (aType != aType.getSerializedClassMeta(BeanTraverseSession.this))
257            sb.append('/').append(aType.getSerializedClassMeta(BeanTraverseSession.this).toString(simple));
258         return sb.toString();
259      }
260   }
261
262   /**
263    * Returns the current stack trace.
264    *
265    * @param full
266    *    If <jk>true</jk>, returns a full stack trace.
267    * @return The current stack trace.
268    */
269   protected String getStack(boolean full) {
270      StringBuilder sb = new StringBuilder();
271      for (StackElement e : stack) {
272         if (full) {
273            sb.append("\n\t");
274            for (int i = 1; i < e.depth; i++)
275               sb.append("  ");
276            if (e.depth > 0)
277               sb.append("->");
278            sb.append(e.toString(false));
279         } else {
280            sb.append(" > ").append(e.toString(true));
281         }
282      }
283      return sb.toString();
284   }
285
286   /**
287    * Returns information used to determine at what location in the parse a failure occurred.
288    *
289    * @return A map, typically containing something like <c>{line:123,column:456,currentProperty:"foobar"}</c>
290    */
291   public final OMap getLastLocation() {
292      return OMap.of()
293         .asn("currentClass", currentClass)
294         .asn("currentProperty", currentProperty)
295         .ase("stack", stack);
296   }
297
298   //-----------------------------------------------------------------------------------------------------------------
299   // Properties
300   //-----------------------------------------------------------------------------------------------------------------
301
302   /**
303    * Configuration property:  Automatically detect POJO recursions.
304    *
305    * @see BeanTraverseContext#BEANTRAVERSE_detectRecursions
306    * @return
307    *    <jk>true</jk> if recursions should be checked for during traversal.
308    */
309   protected final boolean isDetectRecursions() {
310      return ctx.isDetectRecursions();
311   }
312
313   /**
314    * Configuration property:  Ignore recursion errors.
315    *
316    * @see BeanTraverseContext#BEANTRAVERSE_ignoreRecursions
317    * @return
318    *    <jk>true</jk> if when we encounter the same object when traversing a tree, we set the value to <jk>null</jk>.
319    *    <br>Otherwise, a {@link BeanRecursionException} is thrown with the message <js>"Recursion occurred, stack=..."</js>.
320    */
321   protected final boolean isIgnoreRecursions() {
322      return ctx.isIgnoreRecursions();
323   }
324
325   /**
326    * Configuration property:  Initial depth.
327    *
328    * @see BeanTraverseContext#BEANTRAVERSE_initialDepth
329    * @return
330    *    The initial indentation level at the root.
331    */
332   protected final int getInitialDepth() {
333      return ctx.getInitialDepth();
334   }
335
336   /**
337    * Configuration property:  Max traversal depth.
338    *
339    * @see BeanTraverseContext#BEANTRAVERSE_maxDepth
340    * @return
341    *    The depth at which traversal is aborted if depth is reached in the POJO tree.
342    * <br>If this depth is exceeded, an exception is thrown.
343    */
344   protected final int getMaxDepth() {
345      return ctx.getMaxDepth();
346   }
347
348   //-----------------------------------------------------------------------------------------------------------------
349   // Other methods
350   //-----------------------------------------------------------------------------------------------------------------
351
352   @Override /* Session */
353   public OMap toMap() {
354      return super.toMap()
355         .a("BeanTraverseSession", new DefaultFilteringOMap()
356         );
357   }
358}