001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau;
018
019import static org.apache.juneau.collections.JsonMap.*;
020import static org.apache.juneau.internal.ClassUtils.*;
021
022import java.text.*;
023import java.util.*;
024import java.util.function.*;
025
026import org.apache.juneau.collections.*;
027import org.apache.juneau.common.utils.*;
028import org.apache.juneau.internal.*;
029
030/**
031 * ContextSession that lives for the duration of a single use of {@link BeanTraverseContext}.
032 *
033 * <p>
034 * Used by serializers and other classes that traverse POJOs for the following purposes:
035 * <ul class='spaced-list'>
036 *    <li>
037 *       Keeping track of how deep it is in a model for indentation purposes.
038 *    <li>
039 *       Ensuring infinite loops don't occur by setting a limit on how deep to traverse a model.
040 *    <li>
041 *       Ensuring infinite loops don't occur from loops in the model (when detectRecursions is enabled.
042 * </ul>
043 *
044 * <h5 class='section'>Notes:</h5><ul>
045 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
046 * </ul>
047 *
048 * <h5 class='section'>See Also:</h5><ul>
049 * </ul>
050 */
051public class BeanTraverseSession extends BeanSession {
052
053   //-----------------------------------------------------------------------------------------------------------------
054   // Builder
055   //-----------------------------------------------------------------------------------------------------------------
056
057   /**
058    * Builder class.
059    */
060   public static abstract class Builder extends BeanSession.Builder {
061
062      BeanTraverseContext ctx;
063      int initialDepth;
064
065      /**
066       * Constructor
067       *
068       * @param ctx The context creating this session.
069       */
070      protected Builder(BeanTraverseContext ctx) {
071         super(ctx.getBeanContext());
072         this.ctx = ctx;
073         initialDepth = ctx.initialDepth;
074      }
075      @Override /* Overridden from Builder */
076      public <T> Builder apply(Class<T> type, Consumer<T> apply) {
077         super.apply(type, apply);
078         return this;
079      }
080
081      @Override /* Overridden from Builder */
082      public Builder debug(Boolean value) {
083         super.debug(value);
084         return this;
085      }
086
087      @Override /* Overridden from Builder */
088      public Builder properties(Map<String,Object> value) {
089         super.properties(value);
090         return this;
091      }
092
093      @Override /* Overridden from Builder */
094      public Builder property(String key, Object value) {
095         super.property(key, value);
096         return this;
097      }
098
099      @Override /* Overridden from Builder */
100      public Builder unmodifiable() {
101         super.unmodifiable();
102         return this;
103      }
104
105      @Override /* Overridden from Builder */
106      public Builder locale(Locale value) {
107         super.locale(value);
108         return this;
109      }
110
111      @Override /* Overridden from Builder */
112      public Builder localeDefault(Locale value) {
113         super.localeDefault(value);
114         return this;
115      }
116
117      @Override /* Overridden from Builder */
118      public Builder mediaType(MediaType value) {
119         super.mediaType(value);
120         return this;
121      }
122
123      @Override /* Overridden from Builder */
124      public Builder mediaTypeDefault(MediaType value) {
125         super.mediaTypeDefault(value);
126         return this;
127      }
128
129      @Override /* Overridden from Builder */
130      public Builder timeZone(TimeZone value) {
131         super.timeZone(value);
132         return this;
133      }
134
135      @Override /* Overridden from Builder */
136      public Builder timeZoneDefault(TimeZone value) {
137         super.timeZoneDefault(value);
138         return this;
139      }
140   }
141
142   //-----------------------------------------------------------------------------------------------------------------
143   // Instance
144   //-----------------------------------------------------------------------------------------------------------------
145
146   private final BeanTraverseContext ctx;
147   private final Map<Object,Object> set;                                           // Contains the current objects in the current branch of the model.
148   private final LinkedList<StackElement> stack = new LinkedList<>();              // Contains the current objects in the current branch of the model.
149
150   // Writable properties
151   private boolean isBottom;                                                       // If 'true', then we're at a leaf in the model (i.e. a String, Number, Boolean, or null).
152   private BeanPropertyMeta currentProperty;
153   private ClassMeta<?> currentClass;
154
155   /** The current indentation depth into the model. */
156   public int indent;
157
158   private int depth;
159
160   /**
161    * Constructor.
162    *
163    * @param builder The builder for this object.
164    */
165   protected BeanTraverseSession(Builder builder) {
166      super(builder);
167      ctx = builder.ctx;
168      indent =  builder.initialDepth;
169      if (isDetectRecursions() || isDebug()) {
170         set = new IdentityHashMap<>();
171      } else {
172         set = Collections.emptyMap();
173      }
174   }
175
176   /**
177    * Sets the current bean property being traversed for proper error messages.
178    *
179    * @param currentProperty The current property being traversed.
180    */
181   protected final void setCurrentProperty(BeanPropertyMeta currentProperty) {
182      this.currentProperty = currentProperty;
183   }
184
185   /**
186    * Sets the current class being traversed for proper error messages.
187    *
188    * @param currentClass The current class being traversed.
189    */
190   protected final void setCurrentClass(ClassMeta<?> currentClass) {
191      this.currentClass = currentClass;
192   }
193
194   /**
195    * Push the specified object onto the stack.
196    *
197    * @param attrName The attribute name.
198    * @param o The current object being traversed.
199    * @param eType The expected class type.
200    * @return
201    *    The {@link ClassMeta} of the object so that <c>instanceof</c> operations only need to be performed
202    *    once (since they can be expensive).
203    * @throws BeanRecursionException If recursion occurred.
204    */
205   protected final ClassMeta<?> push(String attrName, Object o, ClassMeta<?> eType) throws BeanRecursionException {
206      indent++;
207      depth++;
208      isBottom = true;
209      if (o == null)
210         return null;
211      Class<?> c = o.getClass();
212      ClassMeta<?> cm = (eType != null && c == eType.getInnerClass()) ? eType : ((o instanceof ClassMeta) ? (ClassMeta<?>)o : getClassMeta(c));
213      if (cm.isCharSequence() || cm.isNumber() || cm.isBoolean())
214         return cm;
215      if (depth > getMaxDepth())
216         return null;
217      if (isDetectRecursions() || isDebug()) {
218         if (willRecurse(attrName, o, cm))
219            return null;
220         isBottom = false;
221         stack.add(new StackElement(stack.size(), attrName, o, cm));
222         set.put(o, o);
223      }
224      return cm;
225   }
226
227   /**
228    * Returns <jk>true</jk> if we're processing the root node.
229    *
230    * <p>
231    * Must be called after {@link #push(String, Object, ClassMeta)} and before {@link #pop()}.
232    *
233    * @return <jk>true</jk> if we're processing the root node.
234    */
235   protected final boolean isRoot() {
236      return depth == 1;
237   }
238
239   /**
240    * Returns <jk>true</jk> if {@link BeanTraverseContext.Builder#detectRecursions()} is enabled, and the specified
241    * object is already higher up in the traversal chain.
242    *
243    * @param attrName The bean property attribute name, or some other identifier.
244    * @param o The object to check for recursion.
245    * @param cm The metadata on the object class.
246    * @return <jk>true</jk> if recursion detected.
247    * @throws BeanRecursionException If recursion occurred.
248    */
249   protected final boolean willRecurse(String attrName, Object o, ClassMeta<?> cm) throws BeanRecursionException {
250      if (! (isDetectRecursions() || isDebug()) || ! set.containsKey(o))
251         return false;
252      if (isIgnoreRecursions() && ! isDebug())
253         return true;
254
255      stack.add(new StackElement(stack.size(), attrName, o, cm));
256      throw new BeanRecursionException("Recursion occurred, stack={0}", getStack(true));
257   }
258
259   /**
260    * Returns <jk>true</jk> if we're about to exceed the max depth for the document.
261    *
262    * @return <jk>true</jk> if we're about to exceed the max depth for the document.
263    */
264   protected final boolean willExceedDepth() {
265      return (depth >= getMaxDepth());
266   }
267
268   /**
269    * Pop an object off the stack.
270    */
271   protected final void pop() {
272      indent--;
273      depth--;
274      if ((isDetectRecursions() || isDebug()) && ! isBottom)  {
275         Object o = stack.removeLast().o;
276         Object o2 = set.remove(o);
277         if (o2 == null)
278            onError(null, "Couldn't remove object of type ''{0}'' on attribute ''{1}'' from object stack.", className(o), stack);
279      }
280      isBottom = false;
281   }
282
283   /**
284    * Same as {@link ClassMeta#isOptional()} but gracefully handles a null {@link ClassMeta}.
285    *
286    * @param cm The meta to check.
287    * @return <jk>true</jk> if the specified meta is an {@link Optional}.
288    */
289   protected final boolean isOptional(ClassMeta<?> cm) {
290      return (cm != null && cm.isOptional());
291   }
292
293   /**
294    * Returns the inner type of an {@link Optional}.
295    *
296    * @param cm The meta to check.
297    * @return The inner type of an {@link Optional}.
298    */
299   protected final ClassMeta<?> getOptionalType(ClassMeta<?> cm) {
300      if (cm.isOptional())
301         return getOptionalType(cm.getElementType());
302      return cm;
303   }
304
305   /**
306    * If the specified object is an {@link Optional}, returns the inner object.
307    *
308    * @param o The object to check.
309    * @return The inner object if it's an {@link Optional}, <jk>null</jk> if it's <jk>null</jk>, or else the same object.
310    */
311   protected final Object getOptionalValue(Object o) {
312      if (o == null)
313         return null;
314      if (o instanceof Optional)
315         return getOptionalValue(((Optional<?>)o).orElse(null));
316      return o;
317   }
318
319   /**
320    * Logs a warning message.
321    *
322    * @param t The throwable that was thrown (if there was one).
323    * @param msg The warning message.
324    * @param args Optional {@link MessageFormat}-style arguments.
325    */
326   protected void onError(Throwable t, String msg, Object... args) {
327      super.addWarning(msg, args);
328   }
329
330   private class StackElement {
331      final int depth;
332      final String name;
333      final Object o;
334      final ClassMeta<?> aType;
335
336      StackElement(int depth, String name, Object o, ClassMeta<?> aType) {
337         this.depth = depth;
338         this.name = name;
339         this.o = o;
340         this.aType = aType;
341      }
342
343      String toString(boolean simple) {
344         StringBuilder sb = new StringBuilder().append('[').append(depth).append(']').append(' ');
345         sb.append(Utils.isEmpty(name) ? "<noname>" : name).append(':');
346         sb.append(aType.toString(simple));
347         if (aType != aType.getSerializedClassMeta(BeanTraverseSession.this))
348            sb.append('/').append(aType.getSerializedClassMeta(BeanTraverseSession.this).toString(simple));
349         return sb.toString();
350      }
351   }
352
353   /**
354    * Returns the current stack trace.
355    *
356    * @param full
357    *    If <jk>true</jk>, returns a full stack trace.
358    * @return The current stack trace.
359    */
360   protected String getStack(boolean full) {
361      StringBuilder sb = new StringBuilder();
362      stack.forEach(x -> {
363         if (full) {
364            sb.append("\n\t");
365            for (int i = 1; i < x.depth; i++)
366               sb.append("  ");
367            if (x.depth > 0)
368               sb.append("->");
369            sb.append(x.toString(false));
370         } else {
371            sb.append(" > ").append(x.toString(true));
372         }
373      });
374      return sb.toString();
375   }
376
377   /**
378    * Returns information used to determine at what location in the parse a failure occurred.
379    *
380    * @return A map, typically containing something like <c>{line:123,column:456,currentProperty:"foobar"}</c>
381    */
382   public final JsonMap getLastLocation() {
383      Predicate<Object> nn = Utils::isNotNull;
384      Predicate<Collection<?>> nec = Utils::isNotEmpty;
385      return JsonMap.create()
386         .appendIf(nn, "currentClass", currentClass)
387         .appendIf(nn, "currentProperty", currentProperty)
388         .appendIf(nec, "stack", stack);
389   }
390
391   //-----------------------------------------------------------------------------------------------------------------
392   // Properties
393   //-----------------------------------------------------------------------------------------------------------------
394
395   /**
396    * Automatically detect POJO recursions.
397    *
398    * @see BeanTraverseContext.Builder#detectRecursions()
399    * @return
400    *    <jk>true</jk> if recursions should be checked for during traversal.
401    */
402   public final boolean isDetectRecursions() {
403      return ctx.isDetectRecursions();
404   }
405
406   /**
407    * Ignore recursion errors.
408    *
409    * @see BeanTraverseContext.Builder#ignoreRecursions()
410    * @return
411    *    <jk>true</jk> if when we encounter the same object when traversing a tree, we set the value to <jk>null</jk>.
412    *    <br>Otherwise, a {@link BeanRecursionException} is thrown with the message <js>"Recursion occurred, stack=..."</js>.
413    */
414   public final boolean isIgnoreRecursions() {
415      return ctx.isIgnoreRecursions();
416   }
417
418   /**
419    * Initial depth.
420    *
421    * @see BeanTraverseContext.Builder#initialDepth(int)
422    * @return
423    *    The initial indentation level at the root.
424    */
425   public final int getInitialDepth() {
426      return ctx.getInitialDepth();
427   }
428
429   /**
430    * Max traversal depth.
431    *
432    * @see BeanTraverseContext.Builder#maxDepth(int)
433    * @return
434    *    The depth at which traversal is aborted if depth is reached in the POJO tree.
435    * <br>If this depth is exceeded, an exception is thrown.
436    */
437   public final int getMaxDepth() {
438      return ctx.getMaxDepth();
439   }
440
441   //-----------------------------------------------------------------------------------------------------------------
442   // Other methods
443   //-----------------------------------------------------------------------------------------------------------------
444
445   @Override /* ContextSession */
446   protected JsonMap properties() {
447      return filteredMap("indent", indent, "depth", depth);
448   }
449}