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.stream; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.io.*; 022import java.lang.reflect.*; 023import java.math.*; 024import java.util.*; 025import java.util.stream.*; 026 027import org.apache.juneau.marshall.*; 028 029/** 030 * Format-neutral POJO walker that re-emits an arbitrary object graph through a {@link TokenWriter}. 031 * 032 * <p> 033 * Used by {@link TokenWriter#object(Object)} on every format that supports the cursor-level POJO 034 * bridge: the walker introspects the value (null / scalar / array / collection / map / bean) and 035 * recursively emits {@link TokenWriter#startObject() startObject} / {@link TokenWriter#fieldName fieldName} 036 * / {@link TokenWriter#string string} / {@link TokenWriter#number(long) number} / 037 * {@link TokenWriter#bool bool} / {@link TokenWriter#nil nil} / {@link TokenWriter#binary binary} 038 * events on the calling writer. No raw bytes are written by the walker; per-format encoding is 039 * the {@link TokenWriter}'s job. 040 * 041 * <h5 class='section'>Notes:</h5><ul> 042 * <li>Object swaps and {@code @Schema} annotations <em>are</em> applied during the walk via 043 * {@link MarshallingContext#toBeanMap(Object)} (e.g. {@code java.time} types are emitted as 044 * ISO-8601 strings). This is the explicit carve-out from the cursor's "purely structural" 045 * rule for the {@link TokenWriter#object(Object) object} bridge. The bean introspection 046 * uses the <b>live session's</b> {@link MarshallingContext} (threaded through 047 * {@link Options#context()}) so swaps / bean-filters / locale / timezone honor the same 048 * per-invocation configuration as {@code serialize(...)}; it falls back to 049 * {@link MarshallingContext#DEFAULT} only when no context is supplied. 050 * <li>Self-referencing graphs are guarded by an identity-based ancestor check: re-entering an 051 * object already on the current walk path throws {@link IllegalStateException} rather than 052 * recursing until the stack overflows. (Shared but acyclic references — the same node 053 * reachable by two distinct paths — are permitted.) 054 * </ul> 055 */ 056@SuppressWarnings({ 057 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 058 "resource" // TokenWriter is a fluent Closeable returning itself; discarding the chained return value is intentional, and the writer is the caller's to close. 059}) 060public final class PojoWalker { 061 062 /** 063 * Databind-level settings honored by {@link PojoWalker}. 064 * 065 * <p> 066 * Built by the format's {@code writeTokens(...)} factory from the calling 067 * {@link org.apache.juneau.marshall.serializer.Serializer.Builder Serializer.Builder} and 068 * passed through to the walker. These are the same settings the POJO databind path 069 * ({@code Serializer.write(...)}) consults; honoring them here means 070 * {@code writeTokens(out).object(o)} produces the same shape as 071 * {@code serializer.write(o)} for the same configuration. 072 * 073 * @param keepNullProperties Whether bean fields with null values are emitted (true) or 074 * skipped (false). 075 * @param trimEmptyMaps Whether bean / map fields whose value is an empty map are skipped. 076 * @param trimEmptyCollections Whether bean / map fields whose value is an empty collection / 077 * array are skipped. 078 * @param sortMaps Whether map / bean property iteration order is sorted by key. 079 * @param sortCollections Whether collection element iteration order is sorted (a sorted copy 080 * is walked). 081 * @param trimStrings Whether emitted scalar strings are trimmed. 082 * @param context The live session's {@link MarshallingContext} used for bean introspection 083 * (swaps / bean-filters / locale / timezone). May be <jk>null</jk>, in which case 084 * {@link MarshallingContext#DEFAULT} is used. 085 */ 086 public record Options( 087 boolean keepNullProperties, 088 boolean trimEmptyMaps, 089 boolean trimEmptyCollections, 090 boolean sortMaps, 091 boolean sortCollections, 092 boolean trimStrings, 093 MarshallingContext context) { 094 095 /** 096 * Default options: emit nulls, no trimming, no sorting, no bound context (falls back to 097 * {@link MarshallingContext#DEFAULT}). Mirrors 098 * {@link org.apache.juneau.marshall.serializer.Serializer.Builder Serializer.Builder}'s 099 * defaults. 100 */ 101 public static final Options DEFAULT = new Options(true, false, false, false, false, false, null); 102 103 /** Returns the bound context, or {@link MarshallingContext#DEFAULT} when none was supplied. */ 104 MarshallingContext contextOrDefault() { 105 return or(context, MarshallingContext.DEFAULT); 106 } 107 } 108 109 private PojoWalker() { 110 // Static-method facade. 111 } 112 113 /** 114 * Walks the given value and emits its structure through the given writer using 115 * {@link Options#DEFAULT default options}. 116 * 117 * @param w The token writer to emit events to. Must not be <jk>null</jk>. 118 * @param value The value to walk. May be <jk>null</jk>. 119 * @throws IOException If the writer's underlying stream fails. 120 */ 121 public static void walk(TokenWriter w, Object value) throws IOException { 122 walk(w, value, Options.DEFAULT); 123 } 124 125 /** 126 * Walks the given value and emits its structure through the given writer using the supplied 127 * options. 128 * 129 * @param w The token writer to emit events to. Must not be <jk>null</jk>. 130 * @param value The value to walk. May be <jk>null</jk>. 131 * @param options The walker's databind-level options. Must not be <jk>null</jk>. 132 * @throws IOException If the writer's underlying stream fails. 133 */ 134 public static void walk(TokenWriter w, Object value, Options options) throws IOException { 135 reqnn("w", w); 136 reqnn("options", options); 137 // Identity-based ancestor set: tracks the objects on the current walk path so a 138 // self-referencing graph throws instead of overflowing the stack. Shared-but-acyclic 139 // references are fine because each node is removed from the set once its subtree completes. 140 walkValue(w, value, options, Collections.newSetFromMap(new IdentityHashMap<>())); 141 } 142 143 private static void walkValue(TokenWriter w, Object value, Options options, Set<Object> ancestors) throws IOException { 144 if (value == null) { 145 w.nil(); 146 return; 147 } 148 if (value instanceof CharSequence value2) { 149 var s = value2.toString(); 150 if (options.trimStrings) 151 s = s.trim(); 152 w.string(s); 153 return; 154 } 155 if (value instanceof Boolean value2) { 156 w.bool(value2); 157 return; 158 } 159 if (value instanceof byte[] value2) { 160 w.binary(value2); 161 return; 162 } 163 if (value instanceof Number value2) { 164 writeNumber(w, value2); 165 return; 166 } 167 if (value instanceof Character value2) { 168 w.string(value2.toString()); 169 return; 170 } 171 if (value instanceof Map<?,?> value2) { 172 enterAncestor(value, ancestors); 173 try { 174 walkMap(w, value2, options, ancestors); 175 } finally { 176 ancestors.remove(value); 177 } 178 return; 179 } 180 if (value instanceof Collection<?> value2) { 181 enterAncestor(value, ancestors); 182 try { 183 walkCollection(w, value2, options, ancestors); 184 } finally { 185 ancestors.remove(value); 186 } 187 return; 188 } 189 var cls = value.getClass(); 190 if (cls.isArray()) { 191 enterAncestor(value, ancestors); 192 try { 193 walkArray(w, value, options, ancestors); 194 } finally { 195 ancestors.remove(value); 196 } 197 return; 198 } 199 if (value instanceof Iterable<?> value2) { 200 enterAncestor(value, ancestors); 201 try { 202 walkIterable(w, value2, options, ancestors); 203 } finally { 204 ancestors.remove(value); 205 } 206 return; 207 } 208 if (value instanceof Enum<?> value2) { 209 w.string(value2.name()); 210 return; 211 } 212 // Inspect the type via the live session's MarshallingContext to decide whether to walk it 213 // as a bean or to fall through to its swapped scalar representation. Known non-bean types 214 // (Date, Calendar, Temporal, Duration, Period, URI/URL, UUID, Class, etc.) cannot be walked 215 // via toBeanMap; emit them as their string form, which is the same form the databind path 216 // produces via DefaultSwaps. 217 var mc = options.contextOrDefault(); 218 var cm = mc.getClassMeta(value.getClass()); 219 if (cm.isBean()) { 220 enterAncestor(value, ancestors); 221 try { 222 walkMap(w, mc.toBeanMap(value), options, ancestors); 223 } finally { 224 ancestors.remove(value); 225 } 226 return; 227 } 228 // Non-bean fallback: emit as string. For java.time / Date / UUID / Class etc. the 229 // toString() form is the canonical ISO-8601 / hex / FQN representation. 230 w.string(value.toString()); 231 } 232 233 /** Adds the value to the ancestor set, throwing if it is already on the current walk path. */ 234 private static void enterAncestor(Object value, Set<Object> ancestors) { 235 if (!ancestors.add(value)) 236 throw isex("Recursion detected while walking a self-referencing object graph. Offending type: %s", value.getClass().getName()); 237 } 238 239 private static void writeNumber(TokenWriter w, Number n) throws IOException { 240 if (n instanceof Long n2) { w.number(n2.longValue()); return; } 241 if (n instanceof Integer n2) { w.number(n2.longValue()); return; } 242 if (n instanceof Short n2) { w.number(n2.longValue()); return; } 243 if (n instanceof Byte n2) { w.number(n2.longValue()); return; } 244 if (n instanceof Double n2) { w.number(n2.doubleValue()); return; } 245 if (n instanceof Float n2) { w.number(n2.doubleValue()); return; } 246 if (n instanceof BigDecimal n2) { w.number(n2); return; } 247 if (n instanceof BigInteger n2) { w.number(n2); return; } 248 w.number(n); 249 } 250 251 private static void walkMap(TokenWriter w, Map<?,?> m, Options options, Set<Object> ancestors) throws IOException { 252 w.startObject(); 253 Iterable<? extends Map.Entry<?,?>> entries = m.entrySet(); 254 if (options.sortMaps && !m.isEmpty()) 255 entries = sortedEntries(m); 256 for (var entry : entries) { 257 var key = entry.getKey(); 258 var value = entry.getValue(); 259 if (skipMapEntry(value, options)) 260 continue; 261 w.fieldName(key == null ? "null" : key.toString()); 262 walkValue(w, value, options, ancestors); 263 } 264 w.endObject(); 265 } 266 267 private static List<Map.Entry<Object,Object>> sortedEntries(Map<?,?> m) { 268 List<Map.Entry<Object,Object>> list = m.entrySet().stream() 269 .<Map.Entry<Object,Object>>map(entry -> new AbstractMap.SimpleEntry<>(entry.getKey(), entry.getValue())) 270 .collect(Collectors.toCollection(ArrayList::new)); 271 list.sort(Comparator.comparing(e -> { 272 var k = e.getKey(); 273 return k == null ? "" : k.toString(); 274 })); 275 return list; 276 } 277 278 private static boolean skipMapEntry(Object value, Options options) { 279 if (value == null) 280 return !options.keepNullProperties; 281 if (options.trimEmptyMaps && value instanceof Map<?,?> value2 && value2.isEmpty()) 282 return true; 283 if (options.trimEmptyCollections) { 284 if (value instanceof Collection<?> value2 && value2.isEmpty()) 285 return true; 286 if (value.getClass().isArray() && Array.getLength(value) == 0) 287 return true; 288 } 289 return false; 290 } 291 292 private static void walkCollection(TokenWriter w, Collection<?> c, Options options, Set<Object> ancestors) throws IOException { 293 w.startArray(); 294 Iterable<?> items = c; 295 if (options.sortCollections && !c.isEmpty()) { 296 var copy = new ArrayList<Object>(c); 297 copy.sort(Comparator.comparing(o -> o == null ? "" : o.toString())); 298 items = copy; 299 } 300 for (var item : items) 301 walkValue(w, item, options, ancestors); 302 w.endArray(); 303 } 304 305 private static void walkIterable(TokenWriter w, Iterable<?> it, Options options, Set<Object> ancestors) throws IOException { 306 w.startArray(); 307 for (var item : it) 308 walkValue(w, item, options, ancestors); 309 w.endArray(); 310 } 311 312 private static void walkArray(TokenWriter w, Object array, Options options, Set<Object> ancestors) throws IOException { 313 w.startArray(); 314 var len = Array.getLength(array); 315 if (options.sortCollections && len > 0) { 316 var copy = new ArrayList<Object>(len); 317 for (var i = 0; i < len; i++) 318 copy.add(Array.get(array, i)); 319 copy.sort(Comparator.comparing(o -> o == null ? "" : o.toString())); 320 for (var item : copy) 321 walkValue(w, item, options, ancestors); 322 } else { 323 for (var i = 0; i < len; i++) 324 walkValue(w, Array.get(array, i), options, ancestors); 325 } 326 w.endArray(); 327 } 328}