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 &mdash; the same node
053 *       reachable by two distinct paths &mdash; 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}