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