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 <code>instanceof</code> 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 * Logs a warning message. 169 * 170 * @param t The throwable that was thrown (if there was one). 171 * @param msg The warning message. 172 * @param args Optional {@link MessageFormat}-style arguments. 173 */ 174 protected void onError(Throwable t, String msg, Object... args) { 175 super.addWarning(msg, args); 176 } 177 178 private final class StackElement { 179 final int depth; 180 final String name; 181 final Object o; 182 final ClassMeta<?> aType; 183 184 StackElement(int depth, String name, Object o, ClassMeta<?> aType) { 185 this.depth = depth; 186 this.name = name; 187 this.o = o; 188 this.aType = aType; 189 } 190 191 String toString(boolean simple) { 192 StringBuilder sb = new StringBuilder().append('[').append(depth).append(']').append(' '); 193 sb.append(isEmpty(name) ? "<noname>" : name).append(':'); 194 sb.append(aType.toString(simple)); 195 if (aType != aType.getSerializedClassMeta(BeanTraverseSession.this)) 196 sb.append('/').append(aType.getSerializedClassMeta(BeanTraverseSession.this).toString(simple)); 197 return sb.toString(); 198 } 199 } 200 201 /** 202 * Returns the current stack trace. 203 * 204 * @param full 205 * If <jk>true</jk>, returns a full stack trace. 206 * @return The current stack trace. 207 */ 208 protected String getStack(boolean full) { 209 StringBuilder sb = new StringBuilder(); 210 for (StackElement e : stack) { 211 if (full) { 212 sb.append("\n\t"); 213 for (int i = 1; i < e.depth; i++) 214 sb.append(" "); 215 if (e.depth > 0) 216 sb.append("->"); 217 sb.append(e.toString(false)); 218 } else { 219 sb.append(" > ").append(e.toString(true)); 220 } 221 } 222 return sb.toString(); 223 } 224 225 /** 226 * Returns information used to determine at what location in the parse a failure occurred. 227 * 228 * @return A map, typically containing something like <code>{line:123,column:456,currentProperty:"foobar"}</code> 229 */ 230 public final ObjectMap getLastLocation() { 231 ObjectMap m = new ObjectMap(); 232 if (currentClass != null) 233 m.put("currentClass", currentClass); 234 if (currentProperty != null) 235 m.put("currentProperty", currentProperty); 236 if (stack != null && ! stack.isEmpty()) 237 m.put("stack", stack); 238 return m; 239 } 240 241 //----------------------------------------------------------------------------------------------------------------- 242 // Properties 243 //----------------------------------------------------------------------------------------------------------------- 244 245 /** 246 * Configuration property: Initial depth. 247 * 248 * @see BeanTraverseContext#BEANTRAVERSE_initialDepth 249 * @return 250 * The initial indentation level at the root. 251 */ 252 protected final int getInitialDepth() { 253 return ctx.getInitialDepth(); 254 } 255 256 /** 257 * Configuration property: Max traversal depth. 258 * 259 * @see BeanTraverseContext#BEANTRAVERSE_maxDepth 260 * @return 261 * The depth at which traversal is aborted if depth is reached in the POJO tree. 262 * <br>If this depth is exceeded, an exception is thrown. 263 */ 264 protected final int getMaxDepth() { 265 return ctx.getMaxDepth(); 266 } 267 268 /** 269 * Configuration property: Automatically detect POJO recursions. 270 * 271 * @see BeanTraverseContext#BEANTRAVERSE_detectRecursions 272 * @return 273 * <jk>true</jk> if recursions should be checked for during traversal. 274 */ 275 protected final boolean isDetectRecursions() { 276 return ctx.isDetectRecursions(); 277 } 278 279 /** 280 * Configuration property: Ignore recursion errors. 281 * 282 * @see BeanTraverseContext#BEANTRAVERSE_ignoreRecursions 283 * @return 284 * <jk>true</jk> if when we encounter the same object when traversing a tree, we set the value to <jk>null</jk>. 285 * <br>Otherwise, a {@link BeanRecursionException} is thrown with the message <js>"Recursion occurred, stack=..."</js>. 286 */ 287 protected final boolean isIgnoreRecursions() { 288 return ctx.isIgnoreRecursions(); 289 } 290}