| // *************************************************************************************************************************** |
| // * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * |
| // * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * |
| // * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * |
| // * with the License. You may obtain a copy of the License at * |
| // * * |
| // * http://www.apache.org/licenses/LICENSE-2.0 * |
| // * * |
| // * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * |
| // * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * |
| // * specific language governing permissions and limitations under the License. * |
| // *************************************************************************************************************************** |
| package org.apache.juneau; |
| |
| import static org.apache.juneau.internal.StringUtils.*; |
| |
| import java.text.*; |
| import java.util.*; |
| |
| /** |
| * Session that lives for the duration of a single use of {@link BeanTraverseContext}. |
| * |
| * <p> |
| * Used by serializers and other classes that traverse POJOs for the following purposes: |
| * <ul class='spaced-list'> |
| * <li> |
| * Keeping track of how deep it is in a model for indentation purposes. |
| * <li> |
| * Ensuring infinite loops don't occur by setting a limit on how deep to traverse a model. |
| * <li> |
| * Ensuring infinite loops don't occur from loops in the model (when detectRecursions is enabled. |
| * </ul> |
| * |
| * <p> |
| * This class is NOT thread safe. |
| * It is typically discarded after one-time use although it can be reused within the same thread. |
| */ |
| public class BeanTraverseSession extends BeanSession { |
| |
| private final BeanTraverseContext ctx; |
| private final Map<Object,Object> set; // Contains the current objects in the current branch of the model. |
| private final LinkedList<StackElement> stack = new LinkedList<>(); // Contains the current objects in the current branch of the model. |
| |
| // Writable properties |
| private boolean isBottom; // If 'true', then we're at a leaf in the model (i.e. a String, Number, Boolean, or null). |
| private BeanPropertyMeta currentProperty; |
| private ClassMeta<?> currentClass; |
| |
| /** The current indentation depth into the model. */ |
| public int indent; |
| |
| |
| /** |
| * Create a new session using properties specified in the context. |
| * |
| * @param ctx |
| * The context creating this session object. |
| * The context contains all the configuration settings for this object. |
| * Can be <jk>null</jk>. |
| * @param args |
| * Runtime arguments. |
| * These specify session-level information such as locale and URI context. |
| * It also include session-level properties that override the properties defined on the bean and |
| * serializer contexts. |
| */ |
| protected BeanTraverseSession(BeanTraverseContext ctx, BeanSessionArgs args) { |
| super(ctx, args == null ? BeanSessionArgs.DEFAULT : args); |
| args = args == null ? BeanSessionArgs.DEFAULT : args; |
| this.ctx = ctx; |
| this.indent = getInitialDepth(); |
| if (isDetectRecursions() || isDebug()) { |
| set = new IdentityHashMap<>(); |
| } else { |
| set = Collections.emptyMap(); |
| } |
| } |
| |
| /** |
| * Sets the current bean property being traversed for proper error messages. |
| * |
| * @param currentProperty The current property being traversed. |
| */ |
| protected final void setCurrentProperty(BeanPropertyMeta currentProperty) { |
| this.currentProperty = currentProperty; |
| } |
| |
| /** |
| * Sets the current class being traversed for proper error messages. |
| * |
| * @param currentClass The current class being traversed. |
| */ |
| protected final void setCurrentClass(ClassMeta<?> currentClass) { |
| this.currentClass = currentClass; |
| } |
| |
| /** |
| * Push the specified object onto the stack. |
| * |
| * @param attrName The attribute name. |
| * @param o The current object being traversed. |
| * @param eType The expected class type. |
| * @return |
| * The {@link ClassMeta} of the object so that <c>instanceof</c> operations only need to be performed |
| * once (since they can be expensive). |
| * @throws BeanRecursionException If recursion occurred. |
| */ |
| protected final ClassMeta<?> push(String attrName, Object o, ClassMeta<?> eType) throws BeanRecursionException { |
| indent++; |
| isBottom = true; |
| if (o == null) |
| return null; |
| Class<?> c = o.getClass(); |
| ClassMeta<?> cm = (eType != null && c == eType.getInnerClass()) ? eType : ((o instanceof ClassMeta) ? (ClassMeta<?>)o : getClassMeta(c)); |
| if (cm.isCharSequence() || cm.isNumber() || cm.isBoolean()) |
| return cm; |
| if (isDetectRecursions() || isDebug()) { |
| if (stack.size() > getMaxDepth()) |
| return null; |
| if (willRecurse(attrName, o, cm)) |
| return null; |
| isBottom = false; |
| stack.add(new StackElement(stack.size(), attrName, o, cm)); |
| if (isDebug()) |
| getLogger().info(getStack(false)); |
| set.put(o, o); |
| } |
| return cm; |
| } |
| |
| /** |
| * Returns <jk>true</jk> if {@link BeanTraverseContext#BEANTRAVERSE_detectRecursions} is enabled, and the specified |
| * object is already higher up in the traversal chain. |
| * |
| * @param attrName The bean property attribute name, or some other identifier. |
| * @param o The object to check for recursion. |
| * @param cm The metadata on the object class. |
| * @return <jk>true</jk> if recursion detected. |
| * @throws BeanRecursionException If recursion occurred. |
| */ |
| protected final boolean willRecurse(String attrName, Object o, ClassMeta<?> cm) throws BeanRecursionException { |
| if (! (isDetectRecursions() || isDebug())) |
| return false; |
| if (! set.containsKey(o)) |
| return false; |
| if (isIgnoreRecursions() && ! isDebug()) |
| return true; |
| |
| stack.add(new StackElement(stack.size(), attrName, o, cm)); |
| throw new BeanRecursionException("Recursion occurred, stack={0}", getStack(true)); |
| } |
| |
| /** |
| * Pop an object off the stack. |
| */ |
| protected final void pop() { |
| indent--; |
| if ((isDetectRecursions() || isDebug()) && ! isBottom) { |
| Object o = stack.removeLast().o; |
| Object o2 = set.remove(o); |
| if (o2 == null) |
| onError(null, "Couldn't remove object of type ''{0}'' on attribute ''{1}'' from object stack.", |
| o.getClass().getName(), stack); |
| } |
| isBottom = false; |
| } |
| |
| /** |
| * Same as {@link ClassMeta#isOptional()} but gracefully handles a null {@link ClassMeta}. |
| * |
| * @param cm The meta to check. |
| * @return <jk>true</jk> if the specified meta is an {@link Optional}. |
| */ |
| protected final boolean isOptional(ClassMeta<?> cm) { |
| return (cm != null && cm.isOptional()); |
| } |
| |
| /** |
| * Returns the inner type of an {@link Optional}. |
| * |
| * @param cm The meta to check. |
| * @return The inner type of an {@link Optional}. |
| */ |
| protected final ClassMeta<?> getOptionalType(ClassMeta<?> cm) { |
| if (cm.isOptional()) |
| return getOptionalType(cm.getElementType()); |
| return cm; |
| } |
| |
| /** |
| * If the specified object is an {@link Optional}, returns the inner object. |
| * |
| * @param o The object to check. |
| * @return The inner object if it's an {@link Optional}, <jk>null</jk> if it's <jk>null</jk>, or else the same object. |
| */ |
| protected final Object getOptionalValue(Object o) { |
| if (o == null) |
| return null; |
| if (o instanceof Optional) |
| return getOptionalValue(((Optional<?>)o).orElse(null)); |
| return o; |
| } |
| |
| /** |
| * Logs a warning message. |
| * |
| * @param t The throwable that was thrown (if there was one). |
| * @param msg The warning message. |
| * @param args Optional {@link MessageFormat}-style arguments. |
| */ |
| protected void onError(Throwable t, String msg, Object... args) { |
| super.addWarning(msg, args); |
| } |
| |
| private final class StackElement { |
| final int depth; |
| final String name; |
| final Object o; |
| final ClassMeta<?> aType; |
| |
| StackElement(int depth, String name, Object o, ClassMeta<?> aType) { |
| this.depth = depth; |
| this.name = name; |
| this.o = o; |
| this.aType = aType; |
| } |
| |
| String toString(boolean simple) { |
| StringBuilder sb = new StringBuilder().append('[').append(depth).append(']').append(' '); |
| sb.append(isEmpty(name) ? "<noname>" : name).append(':'); |
| sb.append(aType.toString(simple)); |
| if (aType != aType.getSerializedClassMeta(BeanTraverseSession.this)) |
| sb.append('/').append(aType.getSerializedClassMeta(BeanTraverseSession.this).toString(simple)); |
| return sb.toString(); |
| } |
| } |
| |
| /** |
| * Returns the current stack trace. |
| * |
| * @param full |
| * If <jk>true</jk>, returns a full stack trace. |
| * @return The current stack trace. |
| */ |
| protected String getStack(boolean full) { |
| StringBuilder sb = new StringBuilder(); |
| for (StackElement e : stack) { |
| if (full) { |
| sb.append("\n\t"); |
| for (int i = 1; i < e.depth; i++) |
| sb.append(" "); |
| if (e.depth > 0) |
| sb.append("->"); |
| sb.append(e.toString(false)); |
| } else { |
| sb.append(" > ").append(e.toString(true)); |
| } |
| } |
| return sb.toString(); |
| } |
| |
| /** |
| * Returns information used to determine at what location in the parse a failure occurred. |
| * |
| * @return A map, typically containing something like <c>{line:123,column:456,currentProperty:"foobar"}</c> |
| */ |
| public final ObjectMap getLastLocation() { |
| ObjectMap m = new ObjectMap(); |
| if (currentClass != null) |
| m.put("currentClass", currentClass); |
| if (currentProperty != null) |
| m.put("currentProperty", currentProperty); |
| if (stack != null && ! stack.isEmpty()) |
| m.put("stack", stack); |
| return m; |
| } |
| |
| //----------------------------------------------------------------------------------------------------------------- |
| // Properties |
| //----------------------------------------------------------------------------------------------------------------- |
| |
| /** |
| * Configuration property: Automatically detect POJO recursions. |
| * |
| * @see BeanTraverseContext#BEANTRAVERSE_detectRecursions |
| * @return |
| * <jk>true</jk> if recursions should be checked for during traversal. |
| */ |
| protected final boolean isDetectRecursions() { |
| return ctx.isDetectRecursions(); |
| } |
| |
| /** |
| * Configuration property: Ignore recursion errors. |
| * |
| * @see BeanTraverseContext#BEANTRAVERSE_ignoreRecursions |
| * @return |
| * <jk>true</jk> if when we encounter the same object when traversing a tree, we set the value to <jk>null</jk>. |
| * <br>Otherwise, a {@link BeanRecursionException} is thrown with the message <js>"Recursion occurred, stack=..."</js>. |
| */ |
| protected final boolean isIgnoreRecursions() { |
| return ctx.isIgnoreRecursions(); |
| } |
| |
| /** |
| * Configuration property: Initial depth. |
| * |
| * @see BeanTraverseContext#BEANTRAVERSE_initialDepth |
| * @return |
| * The initial indentation level at the root. |
| */ |
| protected final int getInitialDepth() { |
| return ctx.getInitialDepth(); |
| } |
| |
| /** |
| * Configuration property: Max traversal depth. |
| * |
| * @see BeanTraverseContext#BEANTRAVERSE_maxDepth |
| * @return |
| * The depth at which traversal is aborted if depth is reached in the POJO tree. |
| * <br>If this depth is exceeded, an exception is thrown. |
| */ |
| protected final int getMaxDepth() { |
| return ctx.getMaxDepth(); |
| } |
| |
| //----------------------------------------------------------------------------------------------------------------- |
| // Other methods |
| //----------------------------------------------------------------------------------------------------------------- |
| |
| @Override /* Session */ |
| public ObjectMap toMap() { |
| return super.toMap() |
| .append("BeanTraverseSession", new DefaultFilteringObjectMap() |
| ); |
| } |
| } |