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.commons.utils.AssertionUtils.*;
020import static org.apache.juneau.commons.utils.CollectionUtils.*;
021import static org.apache.juneau.commons.utils.Utils.*;
022
023import java.text.*;
024import java.util.*;
025import java.util.function.*;
026
027import org.apache.juneau.collections.*;
028import org.apache.juneau.commons.collections.FluentMap;
029import org.apache.juneau.commons.utils.*;
030
031/**
032 * ContextSession that lives for the duration of a single use of {@link BeanTraverseContext}.
033 *
034 * <p>
035 * Used by serializers and other classes that traverse POJOs for the following purposes:
036 * <ul class='spaced-list'>
037 *    <li>
038 *       Keeping track of how deep it is in a model for indentation purposes.
039 *    <li>
040 *       Ensuring infinite loops don't occur by setting a limit on how deep to traverse a model.
041 *    <li>
042 *       Ensuring infinite loops don't occur from loops in the model (when detectRecursions is enabled.
043 * </ul>
044 *
045 * <h5 class='section'>Notes:</h5><ul>
046 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
047 * </ul>
048 *
049 */
050public class BeanTraverseSession extends BeanSession {
051   /**
052    * Builder class.
053    */
054   public static abstract class Builder extends BeanSession.Builder {
055
056      private BeanTraverseContext ctx;
057      private int initialDepth;
058
059      /**
060       * Constructor
061       *
062       * @param ctx The context creating this session.
063       *    <br>Cannot be <jk>null</jk>.
064       */
065      protected Builder(BeanTraverseContext ctx) {
066         super(assertArgNotNull("ctx", ctx).getBeanContext());
067         this.ctx = ctx;
068         initialDepth = ctx.getInitialDepth();
069      }
070
071      @Override /* Overridden from Builder */
072      public <T> Builder apply(Class<T> type, Consumer<T> apply) {
073         super.apply(type, apply);
074         return this;
075      }
076
077      @Override /* Overridden from Builder */
078      public Builder debug(Boolean value) {
079         super.debug(value);
080         return this;
081      }
082
083      @Override /* Overridden from Builder */
084      public Builder locale(Locale value) {
085         super.locale(value);
086         return this;
087      }
088
089
090      @Override /* Overridden from Builder */
091      public Builder mediaType(MediaType value) {
092         super.mediaType(value);
093         return this;
094      }
095
096      @Override /* Overridden from Builder */
097      public Builder mediaTypeDefault(MediaType value) {
098         super.mediaTypeDefault(value);
099         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}