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