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