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 020import org.apache.juneau.collections.*; 021 022/** 023 * Session that lives for the duration of a single use of {@link BeanTraverseContext}. 024 * 025 * <p> 026 * Used by serializers and other classes that traverse POJOs for the following purposes: 027 * <ul class='spaced-list'> 028 * <li> 029 * Keeping track of how deep it is in a model for indentation purposes. 030 * <li> 031 * Ensuring infinite loops don't occur by setting a limit on how deep to traverse a model. 032 * <li> 033 * Ensuring infinite loops don't occur from loops in the model (when detectRecursions is enabled. 034 * </ul> 035 * 036 * <p> 037 * This class is NOT thread safe. 038 * It is typically discarded after one-time use although it can be reused within the same thread. 039 */ 040public class BeanTraverseSession extends BeanSession { 041 042 private final BeanTraverseContext ctx; 043 private final Map<Object,Object> set; // Contains the current objects in the current branch of the model. 044 private final LinkedList<StackElement> stack = new LinkedList<>(); // Contains the current objects in the current branch of the model. 045 046 // Writable properties 047 private boolean isBottom; // If 'true', then we're at a leaf in the model (i.e. a String, Number, Boolean, or null). 048 private BeanPropertyMeta currentProperty; 049 private ClassMeta<?> currentClass; 050 051 /** The current indentation depth into the model. */ 052 public int indent; 053 054 private int depth; 055 056 057 /** 058 * Create a new session using properties specified in the context. 059 * 060 * @param ctx 061 * The context creating this session object. 062 * The context contains all the configuration settings for this object. 063 * Can be <jk>null</jk>. 064 * @param args 065 * Runtime arguments. 066 * These specify session-level information such as locale and URI context. 067 * It also include session-level properties that override the properties defined on the bean and 068 * serializer contexts. 069 */ 070 protected BeanTraverseSession(BeanTraverseContext ctx, BeanSessionArgs args) { 071 super(ctx, args == null ? BeanSessionArgs.DEFAULT : args); 072 args = args == null ? BeanSessionArgs.DEFAULT : args; 073 this.ctx = ctx; 074 this.indent = getInitialDepth(); 075 if (isDetectRecursions() || isDebug()) { 076 set = new IdentityHashMap<>(); 077 } else { 078 set = Collections.emptyMap(); 079 } 080 } 081 082 /** 083 * Sets the current bean property being traversed for proper error messages. 084 * 085 * @param currentProperty The current property being traversed. 086 */ 087 protected final void setCurrentProperty(BeanPropertyMeta currentProperty) { 088 this.currentProperty = currentProperty; 089 } 090 091 /** 092 * Sets the current class being traversed for proper error messages. 093 * 094 * @param currentClass The current class being traversed. 095 */ 096 protected final void setCurrentClass(ClassMeta<?> currentClass) { 097 this.currentClass = currentClass; 098 } 099 100 /** 101 * Push the specified object onto the stack. 102 * 103 * @param attrName The attribute name. 104 * @param o The current object being traversed. 105 * @param eType The expected class type. 106 * @return 107 * The {@link ClassMeta} of the object so that <c>instanceof</c> operations only need to be performed 108 * once (since they can be expensive). 109 * @throws BeanRecursionException If recursion occurred. 110 */ 111 protected final ClassMeta<?> push(String attrName, Object o, ClassMeta<?> eType) throws BeanRecursionException { 112 indent++; 113 depth++; 114 isBottom = true; 115 if (o == null) 116 return null; 117 Class<?> c = o.getClass(); 118 ClassMeta<?> cm = (eType != null && c == eType.getInnerClass()) ? eType : ((o instanceof ClassMeta) ? (ClassMeta<?>)o : getClassMeta(c)); 119 if (cm.isCharSequence() || cm.isNumber() || cm.isBoolean()) 120 return cm; 121 if (depth > getMaxDepth()) 122 return null; 123 if (isDetectRecursions() || isDebug()) { 124 if (willRecurse(attrName, o, cm)) 125 return null; 126 isBottom = false; 127 stack.add(new StackElement(stack.size(), attrName, o, cm)); 128 set.put(o, o); 129 } 130 return cm; 131 } 132 133 /** 134 * Returns <jk>true</jk> if we're processing the root node. 135 * 136 * <p> 137 * Must be called after {@link #push(String, Object, ClassMeta)} and before {@link #pop()}. 138 * 139 * @return <jk>true</jk> if we're processing the root node. 140 */ 141 protected final boolean isRoot() { 142 return depth == 1; 143 } 144 145 /** 146 * Returns <jk>true</jk> if {@link BeanTraverseContext#BEANTRAVERSE_detectRecursions} is enabled, and the specified 147 * object is already higher up in the traversal chain. 148 * 149 * @param attrName The bean property attribute name, or some other identifier. 150 * @param o The object to check for recursion. 151 * @param cm The metadata on the object class. 152 * @return <jk>true</jk> if recursion detected. 153 * @throws BeanRecursionException If recursion occurred. 154 */ 155 protected final boolean willRecurse(String attrName, Object o, ClassMeta<?> cm) throws BeanRecursionException { 156 if (! (isDetectRecursions() || isDebug())) 157 return false; 158 if (! set.containsKey(o)) 159 return false; 160 if (isIgnoreRecursions() && ! isDebug()) 161 return true; 162 163 stack.add(new StackElement(stack.size(), attrName, o, cm)); 164 throw new BeanRecursionException("Recursion occurred, stack={0}", getStack(true)); 165 } 166 167 /** 168 * Returns <jk>true</jk> if we're about to exceed the max depth for the document. 169 * 170 * @return <jk>true</jk> if we're about to exceed the max depth for the document. 171 */ 172 protected final boolean willExceedDepth() { 173 return (depth >= getMaxDepth()); 174 } 175 176 /** 177 * Pop an object off the stack. 178 */ 179 protected final void pop() { 180 indent--; 181 depth--; 182 if ((isDetectRecursions() || isDebug()) && ! isBottom) { 183 Object o = stack.removeLast().o; 184 Object o2 = set.remove(o); 185 if (o2 == null) 186 onError(null, "Couldn't remove object of type ''{0}'' on attribute ''{1}'' from object stack.", 187 o.getClass().getName(), stack); 188 } 189 isBottom = false; 190 } 191 192 /** 193 * Same as {@link ClassMeta#isOptional()} but gracefully handles a null {@link ClassMeta}. 194 * 195 * @param cm The meta to check. 196 * @return <jk>true</jk> if the specified meta is an {@link Optional}. 197 */ 198 protected final boolean isOptional(ClassMeta<?> cm) { 199 return (cm != null && cm.isOptional()); 200 } 201 202 /** 203 * Returns the inner type of an {@link Optional}. 204 * 205 * @param cm The meta to check. 206 * @return The inner type of an {@link Optional}. 207 */ 208 protected final ClassMeta<?> getOptionalType(ClassMeta<?> cm) { 209 if (cm.isOptional()) 210 return getOptionalType(cm.getElementType()); 211 return cm; 212 } 213 214 /** 215 * If the specified object is an {@link Optional}, returns the inner object. 216 * 217 * @param o The object to check. 218 * @return The inner object if it's an {@link Optional}, <jk>null</jk> if it's <jk>null</jk>, or else the same object. 219 */ 220 protected final Object getOptionalValue(Object o) { 221 if (o == null) 222 return null; 223 if (o instanceof Optional) 224 return getOptionalValue(((Optional<?>)o).orElse(null)); 225 return o; 226 } 227 228 /** 229 * Logs a warning message. 230 * 231 * @param t The throwable that was thrown (if there was one). 232 * @param msg The warning message. 233 * @param args Optional {@link MessageFormat}-style arguments. 234 */ 235 protected void onError(Throwable t, String msg, Object... args) { 236 super.addWarning(msg, args); 237 } 238 239 private final class StackElement { 240 final int depth; 241 final String name; 242 final Object o; 243 final ClassMeta<?> aType; 244 245 StackElement(int depth, String name, Object o, ClassMeta<?> aType) { 246 this.depth = depth; 247 this.name = name; 248 this.o = o; 249 this.aType = aType; 250 } 251 252 String toString(boolean simple) { 253 StringBuilder sb = new StringBuilder().append('[').append(depth).append(']').append(' '); 254 sb.append(isEmpty(name) ? "<noname>" : name).append(':'); 255 sb.append(aType.toString(simple)); 256 if (aType != aType.getSerializedClassMeta(BeanTraverseSession.this)) 257 sb.append('/').append(aType.getSerializedClassMeta(BeanTraverseSession.this).toString(simple)); 258 return sb.toString(); 259 } 260 } 261 262 /** 263 * Returns the current stack trace. 264 * 265 * @param full 266 * If <jk>true</jk>, returns a full stack trace. 267 * @return The current stack trace. 268 */ 269 protected String getStack(boolean full) { 270 StringBuilder sb = new StringBuilder(); 271 for (StackElement e : stack) { 272 if (full) { 273 sb.append("\n\t"); 274 for (int i = 1; i < e.depth; i++) 275 sb.append(" "); 276 if (e.depth > 0) 277 sb.append("->"); 278 sb.append(e.toString(false)); 279 } else { 280 sb.append(" > ").append(e.toString(true)); 281 } 282 } 283 return sb.toString(); 284 } 285 286 /** 287 * Returns information used to determine at what location in the parse a failure occurred. 288 * 289 * @return A map, typically containing something like <c>{line:123,column:456,currentProperty:"foobar"}</c> 290 */ 291 public final OMap getLastLocation() { 292 return OMap.of() 293 .asn("currentClass", currentClass) 294 .asn("currentProperty", currentProperty) 295 .ase("stack", stack); 296 } 297 298 //----------------------------------------------------------------------------------------------------------------- 299 // Properties 300 //----------------------------------------------------------------------------------------------------------------- 301 302 /** 303 * Configuration property: Automatically detect POJO recursions. 304 * 305 * @see BeanTraverseContext#BEANTRAVERSE_detectRecursions 306 * @return 307 * <jk>true</jk> if recursions should be checked for during traversal. 308 */ 309 protected final boolean isDetectRecursions() { 310 return ctx.isDetectRecursions(); 311 } 312 313 /** 314 * Configuration property: Ignore recursion errors. 315 * 316 * @see BeanTraverseContext#BEANTRAVERSE_ignoreRecursions 317 * @return 318 * <jk>true</jk> if when we encounter the same object when traversing a tree, we set the value to <jk>null</jk>. 319 * <br>Otherwise, a {@link BeanRecursionException} is thrown with the message <js>"Recursion occurred, stack=..."</js>. 320 */ 321 protected final boolean isIgnoreRecursions() { 322 return ctx.isIgnoreRecursions(); 323 } 324 325 /** 326 * Configuration property: Initial depth. 327 * 328 * @see BeanTraverseContext#BEANTRAVERSE_initialDepth 329 * @return 330 * The initial indentation level at the root. 331 */ 332 protected final int getInitialDepth() { 333 return ctx.getInitialDepth(); 334 } 335 336 /** 337 * Configuration property: Max traversal depth. 338 * 339 * @see BeanTraverseContext#BEANTRAVERSE_maxDepth 340 * @return 341 * The depth at which traversal is aborted if depth is reached in the POJO tree. 342 * <br>If this depth is exceeded, an exception is thrown. 343 */ 344 protected final int getMaxDepth() { 345 return ctx.getMaxDepth(); 346 } 347 348 //----------------------------------------------------------------------------------------------------------------- 349 // Other methods 350 //----------------------------------------------------------------------------------------------------------------- 351 352 @Override /* Session */ 353 public OMap toMap() { 354 return super.toMap() 355 .a("BeanTraverseSession", new DefaultFilteringOMap() 356 ); 357 } 358}