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