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