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