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