001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau; 018 019import static org.apache.juneau.collections.JsonMap.*; 020import static org.apache.juneau.internal.ClassUtils.*; 021 022import java.text.*; 023import java.util.*; 024import java.util.function.*; 025 026import org.apache.juneau.collections.*; 027import org.apache.juneau.common.utils.*; 028import org.apache.juneau.internal.*; 029 030/** 031 * ContextSession that lives for the duration of a single use of {@link BeanTraverseContext}. 032 * 033 * <p> 034 * Used by serializers and other classes that traverse POJOs for the following purposes: 035 * <ul class='spaced-list'> 036 * <li> 037 * Keeping track of how deep it is in a model for indentation purposes. 038 * <li> 039 * Ensuring infinite loops don't occur by setting a limit on how deep to traverse a model. 040 * <li> 041 * Ensuring infinite loops don't occur from loops in the model (when detectRecursions is enabled. 042 * </ul> 043 * 044 * <h5 class='section'>Notes:</h5><ul> 045 * <li class='warn'>This class is not thread safe and is typically discarded after one use. 046 * </ul> 047 * 048 * <h5 class='section'>See Also:</h5><ul> 049 * </ul> 050 */ 051public class BeanTraverseSession extends BeanSession { 052 053 //----------------------------------------------------------------------------------------------------------------- 054 // Builder 055 //----------------------------------------------------------------------------------------------------------------- 056 057 /** 058 * Builder class. 059 */ 060 public static abstract class Builder extends BeanSession.Builder { 061 062 BeanTraverseContext ctx; 063 int initialDepth; 064 065 /** 066 * Constructor 067 * 068 * @param ctx The context creating this session. 069 */ 070 protected Builder(BeanTraverseContext ctx) { 071 super(ctx.getBeanContext()); 072 this.ctx = ctx; 073 initialDepth = ctx.initialDepth; 074 } 075 @Override /* Overridden from Builder */ 076 public <T> Builder apply(Class<T> type, Consumer<T> apply) { 077 super.apply(type, apply); 078 return this; 079 } 080 081 @Override /* Overridden from Builder */ 082 public Builder debug(Boolean value) { 083 super.debug(value); 084 return this; 085 } 086 087 @Override /* Overridden from Builder */ 088 public Builder properties(Map<String,Object> value) { 089 super.properties(value); 090 return this; 091 } 092 093 @Override /* Overridden from Builder */ 094 public Builder property(String key, Object value) { 095 super.property(key, value); 096 return this; 097 } 098 099 @Override /* Overridden from Builder */ 100 public Builder unmodifiable() { 101 super.unmodifiable(); 102 return this; 103 } 104 105 @Override /* Overridden from Builder */ 106 public Builder locale(Locale value) { 107 super.locale(value); 108 return this; 109 } 110 111 @Override /* Overridden from Builder */ 112 public Builder localeDefault(Locale value) { 113 super.localeDefault(value); 114 return this; 115 } 116 117 @Override /* Overridden from Builder */ 118 public Builder mediaType(MediaType value) { 119 super.mediaType(value); 120 return this; 121 } 122 123 @Override /* Overridden from Builder */ 124 public Builder mediaTypeDefault(MediaType value) { 125 super.mediaTypeDefault(value); 126 return this; 127 } 128 129 @Override /* Overridden from Builder */ 130 public Builder timeZone(TimeZone value) { 131 super.timeZone(value); 132 return this; 133 } 134 135 @Override /* Overridden from Builder */ 136 public Builder timeZoneDefault(TimeZone value) { 137 super.timeZoneDefault(value); 138 return this; 139 } 140 } 141 142 //----------------------------------------------------------------------------------------------------------------- 143 // Instance 144 //----------------------------------------------------------------------------------------------------------------- 145 146 private final BeanTraverseContext ctx; 147 private final Map<Object,Object> set; // Contains the current objects in the current branch of the model. 148 private final LinkedList<StackElement> stack = new LinkedList<>(); // Contains the current objects in the current branch of the model. 149 150 // Writable properties 151 private boolean isBottom; // If 'true', then we're at a leaf in the model (i.e. a String, Number, Boolean, or null). 152 private BeanPropertyMeta currentProperty; 153 private ClassMeta<?> currentClass; 154 155 /** The current indentation depth into the model. */ 156 public int indent; 157 158 private int depth; 159 160 /** 161 * Constructor. 162 * 163 * @param builder The builder for this object. 164 */ 165 protected BeanTraverseSession(Builder builder) { 166 super(builder); 167 ctx = builder.ctx; 168 indent = builder.initialDepth; 169 if (isDetectRecursions() || isDebug()) { 170 set = new IdentityHashMap<>(); 171 } else { 172 set = Collections.emptyMap(); 173 } 174 } 175 176 /** 177 * Sets the current bean property being traversed for proper error messages. 178 * 179 * @param currentProperty The current property being traversed. 180 */ 181 protected final void setCurrentProperty(BeanPropertyMeta currentProperty) { 182 this.currentProperty = currentProperty; 183 } 184 185 /** 186 * Sets the current class being traversed for proper error messages. 187 * 188 * @param currentClass The current class being traversed. 189 */ 190 protected final void setCurrentClass(ClassMeta<?> currentClass) { 191 this.currentClass = currentClass; 192 } 193 194 /** 195 * Push the specified object onto the stack. 196 * 197 * @param attrName The attribute name. 198 * @param o The current object being traversed. 199 * @param eType The expected class type. 200 * @return 201 * The {@link ClassMeta} of the object so that <c>instanceof</c> operations only need to be performed 202 * once (since they can be expensive). 203 * @throws BeanRecursionException If recursion occurred. 204 */ 205 protected final ClassMeta<?> push(String attrName, Object o, ClassMeta<?> eType) throws BeanRecursionException { 206 indent++; 207 depth++; 208 isBottom = true; 209 if (o == null) 210 return null; 211 Class<?> c = o.getClass(); 212 ClassMeta<?> cm = (eType != null && c == eType.getInnerClass()) ? eType : ((o instanceof ClassMeta) ? (ClassMeta<?>)o : getClassMeta(c)); 213 if (cm.isCharSequence() || cm.isNumber() || cm.isBoolean()) 214 return cm; 215 if (depth > getMaxDepth()) 216 return null; 217 if (isDetectRecursions() || isDebug()) { 218 if (willRecurse(attrName, o, cm)) 219 return null; 220 isBottom = false; 221 stack.add(new StackElement(stack.size(), attrName, o, cm)); 222 set.put(o, o); 223 } 224 return cm; 225 } 226 227 /** 228 * Returns <jk>true</jk> if we're processing the root node. 229 * 230 * <p> 231 * Must be called after {@link #push(String, Object, ClassMeta)} and before {@link #pop()}. 232 * 233 * @return <jk>true</jk> if we're processing the root node. 234 */ 235 protected final boolean isRoot() { 236 return depth == 1; 237 } 238 239 /** 240 * Returns <jk>true</jk> if {@link BeanTraverseContext.Builder#detectRecursions()} is enabled, and the specified 241 * object is already higher up in the traversal chain. 242 * 243 * @param attrName The bean property attribute name, or some other identifier. 244 * @param o The object to check for recursion. 245 * @param cm The metadata on the object class. 246 * @return <jk>true</jk> if recursion detected. 247 * @throws BeanRecursionException If recursion occurred. 248 */ 249 protected final boolean willRecurse(String attrName, Object o, ClassMeta<?> cm) throws BeanRecursionException { 250 if (! (isDetectRecursions() || isDebug()) || ! set.containsKey(o)) 251 return false; 252 if (isIgnoreRecursions() && ! isDebug()) 253 return true; 254 255 stack.add(new StackElement(stack.size(), attrName, o, cm)); 256 throw new BeanRecursionException("Recursion occurred, stack={0}", getStack(true)); 257 } 258 259 /** 260 * Returns <jk>true</jk> if we're about to exceed the max depth for the document. 261 * 262 * @return <jk>true</jk> if we're about to exceed the max depth for the document. 263 */ 264 protected final boolean willExceedDepth() { 265 return (depth >= getMaxDepth()); 266 } 267 268 /** 269 * Pop an object off the stack. 270 */ 271 protected final void pop() { 272 indent--; 273 depth--; 274 if ((isDetectRecursions() || isDebug()) && ! isBottom) { 275 Object o = stack.removeLast().o; 276 Object o2 = set.remove(o); 277 if (o2 == null) 278 onError(null, "Couldn't remove object of type ''{0}'' on attribute ''{1}'' from object stack.", className(o), stack); 279 } 280 isBottom = false; 281 } 282 283 /** 284 * Same as {@link ClassMeta#isOptional()} but gracefully handles a null {@link ClassMeta}. 285 * 286 * @param cm The meta to check. 287 * @return <jk>true</jk> if the specified meta is an {@link Optional}. 288 */ 289 protected final boolean isOptional(ClassMeta<?> cm) { 290 return (cm != null && cm.isOptional()); 291 } 292 293 /** 294 * Returns the inner type of an {@link Optional}. 295 * 296 * @param cm The meta to check. 297 * @return The inner type of an {@link Optional}. 298 */ 299 protected final ClassMeta<?> getOptionalType(ClassMeta<?> cm) { 300 if (cm.isOptional()) 301 return getOptionalType(cm.getElementType()); 302 return cm; 303 } 304 305 /** 306 * If the specified object is an {@link Optional}, returns the inner object. 307 * 308 * @param o The object to check. 309 * @return The inner object if it's an {@link Optional}, <jk>null</jk> if it's <jk>null</jk>, or else the same object. 310 */ 311 protected final Object getOptionalValue(Object o) { 312 if (o == null) 313 return null; 314 if (o instanceof Optional) 315 return getOptionalValue(((Optional<?>)o).orElse(null)); 316 return o; 317 } 318 319 /** 320 * Logs a warning message. 321 * 322 * @param t The throwable that was thrown (if there was one). 323 * @param msg The warning message. 324 * @param args Optional {@link MessageFormat}-style arguments. 325 */ 326 protected void onError(Throwable t, String msg, Object... args) { 327 super.addWarning(msg, args); 328 } 329 330 private class StackElement { 331 final int depth; 332 final String name; 333 final Object o; 334 final ClassMeta<?> aType; 335 336 StackElement(int depth, String name, Object o, ClassMeta<?> aType) { 337 this.depth = depth; 338 this.name = name; 339 this.o = o; 340 this.aType = aType; 341 } 342 343 String toString(boolean simple) { 344 StringBuilder sb = new StringBuilder().append('[').append(depth).append(']').append(' '); 345 sb.append(Utils.isEmpty(name) ? "<noname>" : name).append(':'); 346 sb.append(aType.toString(simple)); 347 if (aType != aType.getSerializedClassMeta(BeanTraverseSession.this)) 348 sb.append('/').append(aType.getSerializedClassMeta(BeanTraverseSession.this).toString(simple)); 349 return sb.toString(); 350 } 351 } 352 353 /** 354 * Returns the current stack trace. 355 * 356 * @param full 357 * If <jk>true</jk>, returns a full stack trace. 358 * @return The current stack trace. 359 */ 360 protected String getStack(boolean full) { 361 StringBuilder sb = new StringBuilder(); 362 stack.forEach(x -> { 363 if (full) { 364 sb.append("\n\t"); 365 for (int i = 1; i < x.depth; i++) 366 sb.append(" "); 367 if (x.depth > 0) 368 sb.append("->"); 369 sb.append(x.toString(false)); 370 } else { 371 sb.append(" > ").append(x.toString(true)); 372 } 373 }); 374 return sb.toString(); 375 } 376 377 /** 378 * Returns information used to determine at what location in the parse a failure occurred. 379 * 380 * @return A map, typically containing something like <c>{line:123,column:456,currentProperty:"foobar"}</c> 381 */ 382 public final JsonMap getLastLocation() { 383 Predicate<Object> nn = Utils::isNotNull; 384 Predicate<Collection<?>> nec = Utils::isNotEmpty; 385 return JsonMap.create() 386 .appendIf(nn, "currentClass", currentClass) 387 .appendIf(nn, "currentProperty", currentProperty) 388 .appendIf(nec, "stack", stack); 389 } 390 391 //----------------------------------------------------------------------------------------------------------------- 392 // Properties 393 //----------------------------------------------------------------------------------------------------------------- 394 395 /** 396 * Automatically detect POJO recursions. 397 * 398 * @see BeanTraverseContext.Builder#detectRecursions() 399 * @return 400 * <jk>true</jk> if recursions should be checked for during traversal. 401 */ 402 public final boolean isDetectRecursions() { 403 return ctx.isDetectRecursions(); 404 } 405 406 /** 407 * Ignore recursion errors. 408 * 409 * @see BeanTraverseContext.Builder#ignoreRecursions() 410 * @return 411 * <jk>true</jk> if when we encounter the same object when traversing a tree, we set the value to <jk>null</jk>. 412 * <br>Otherwise, a {@link BeanRecursionException} is thrown with the message <js>"Recursion occurred, stack=..."</js>. 413 */ 414 public final boolean isIgnoreRecursions() { 415 return ctx.isIgnoreRecursions(); 416 } 417 418 /** 419 * Initial depth. 420 * 421 * @see BeanTraverseContext.Builder#initialDepth(int) 422 * @return 423 * The initial indentation level at the root. 424 */ 425 public final int getInitialDepth() { 426 return ctx.getInitialDepth(); 427 } 428 429 /** 430 * Max traversal depth. 431 * 432 * @see BeanTraverseContext.Builder#maxDepth(int) 433 * @return 434 * The depth at which traversal is aborted if depth is reached in the POJO tree. 435 * <br>If this depth is exceeded, an exception is thrown. 436 */ 437 public final int getMaxDepth() { 438 return ctx.getMaxDepth(); 439 } 440 441 //----------------------------------------------------------------------------------------------------------------- 442 // Other methods 443 //----------------------------------------------------------------------------------------------------------------- 444 445 @Override /* ContextSession */ 446 protected JsonMap properties() { 447 return filteredMap("indent", indent, "depth", depth); 448 } 449}