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.commons.svl;
018
019import java.lang.reflect.*;
020import java.util.*;
021import java.util.function.*;
022
023import org.apache.juneau.commons.svl.functions.*;
024
025/**
026 * Default formatter that turns the typed result of a {@code #{...}} expression into text.
027 *
028 * <p>
029 * <jk>null</jk> becomes the empty string; {@link CharSequence} values are emitted as-is (bare, unquoted); numbers and
030 * booleans use {@code toString} (integral doubles drop the {@code .0}); a {@link Structure} uses its plain-text
031 * {@code toString()}; collections, maps and arrays are written as JSON (keys quoted, strings quoted only inside a
032 * container, <jk>null</jk> as {@code null}).  Any other object inside a container is written as its quoted
033 * {@code String.valueOf} (there is no bean introspection here).
034 *
035 * <p>
036 * Nesting is capped at {@link #DEFAULT_MAX_DEPTH}, each container at the configured element limit, and the whole
037 * call at {@link #MAX_NODES} written values (so a shared acyclic DAG cannot fan out exponentially).  A container that
038 * contains itself (a true cycle on the current path) is reported as a cycle.  Beyond any limit the output is truncated
039 * with a single {@code "..."} marker in lenient mode, or a {@link VarResolverException} is thrown in strict mode;
040 * nothing here can overflow the stack.
041 *
042 * <p>
043 * JSON has no tokens for {@code NaN} or the infinities; inside containers they are written as the quoted strings
044 * {@code "NaN"}, {@code "Infinity"} and {@code "-Infinity"}.  A double or float that is integral is written without the
045 * decimal point, so {@code -0.0} is written as {@code 0}.  The characters U+007F, U+2028 and U+2029 and all control
046 * characters below U+0020 are escaped as {@code \\uXXXX}; lone surrogates are written as-is.
047 */
048@SuppressWarnings({
049   "java:S135", // Container writers use break to stop at the size limit,
050   "java:S3776" // Value writer dispatches over every supported value kind
051})
052public class JsonValueFormatter implements Function<Object,String> {
053
054   /** Default maximum container nesting depth. */
055   public static final int DEFAULT_MAX_DEPTH = 32;
056
057   /** Maximum number of values written by one call. */
058   public static final int MAX_NODES = 100_000;
059
060   private final int maxSize;
061   private final boolean strict;
062
063   /** Constructor using the default collection limit, lenient. */
064   public JsonValueFormatter() {
065      this(VarResolver.DEFAULT_MAX_COLLECTION_SIZE, false);
066   }
067
068   /**
069    * Constructor.
070    *
071    * @param maxSize Maximum elements written per container.
072    * @param strict <jk>true</jk> to throw instead of truncating when a limit is exceeded.
073    */
074   public JsonValueFormatter(int maxSize, boolean strict) {
075      this.maxSize = maxSize;
076      this.strict = strict;
077   }
078
079   /** Per-call state. */
080   private static final class State {
081      int nodes;
082      boolean stop;
083      final Set<Object> path = Collections.newSetFromMap(new IdentityHashMap<>());
084   }
085
086   @Override
087   public String apply(Object o) {
088      if (o == null)
089         return "";
090      if (o instanceof CharSequence || o instanceof Structure)
091         return o.toString();
092      if (o instanceof Number || o instanceof Boolean)
093         return scalar(o);
094      if (isContainer(o)) {
095         var sb = new StringBuilder();
096         write(sb, o, 0, new State());
097         return sb.toString();
098      }
099      return String.valueOf(o);
100   }
101
102   private static boolean isContainer(Object o) {
103      return o instanceof Collection || o instanceof Map || o.getClass().isArray();
104   }
105
106   private static String scalar(Object o) {
107      if (o instanceof Double d)
108         return ArithmeticFunctions.render(d);
109      if (o instanceof Float f) {
110         if (f.isNaN() || f.isInfinite() || f != Math.floor(f))
111            return Float.toString(f);
112         return ArithmeticFunctions.render(f.doubleValue());
113      }
114      return o.toString();
115   }
116
117   private static boolean nonFinite(Object o) {
118      return (o instanceof Double d && (d.isNaN() || d.isInfinite())) || (o instanceof Float f && (f.isNaN() || f.isInfinite()));
119   }
120
121   /** Reports a limit: throws in strict mode, otherwise writes one marker and stops all further output. */
122   private void limit(StringBuilder sb, String what, boolean first, boolean map, State st) {
123      if (strict)
124         throw new VarResolverException("Value formatter %s limit exceeded", what);
125      if (! first)
126         sb.append(',');
127      sb.append(map ? "\"...\":true" : "\"...\"");
128      st.stop = true;
129   }
130
131   private void write(StringBuilder sb, Object o, int depth, State st) {
132      if (st.stop)
133         return;
134      if (++st.nodes > MAX_NODES) {
135         limit(sb, "node", true, false, st);
136         return;
137      }
138      if (o == null) {
139         sb.append("null");
140      } else if (nonFinite(o)) {
141         quote(sb, scalar(o));
142      } else if (o instanceof Number || o instanceof Boolean) {
143         sb.append(scalar(o));
144      } else if (isContainer(o)) {
145         if (depth >= DEFAULT_MAX_DEPTH) {
146            limit(sb, "depth", true, false, st);
147         } else if (! st.path.add(o)) {
148            if (strict)
149               throw new VarResolverException("Value formatter found a cyclic structure");
150            sb.append("\"...\"");
151         } else {
152            try {
153               writeContainer(sb, o, depth, st);
154            } finally {
155               st.path.remove(o);
156            }
157         }
158      } else {
159         quote(sb, String.valueOf(o));
160      }
161   }
162
163   private void writeContainer(StringBuilder sb, Object o, int depth, State st) {
164      if (o instanceof Map<?,?> m) {
165         sb.append('{');
166         int i = 0;
167         for (var e : m.entrySet()) {
168            if (st.stop)
169               break;
170            if (i >= maxSize) {
171               limit(sb, "size", i == 0, true, st);
172               break;
173            }
174            if (i++ > 0) sb.append(',');
175            quote(sb, String.valueOf(e.getKey()));
176            sb.append(':');
177            write(sb, e.getValue(), depth + 1, st);
178         }
179         sb.append('}');
180      } else {
181         sb.append('[');
182         var it = o instanceof Collection<?> c ? c.iterator() : arrayIterator(o);
183         int i = 0;
184         while (it.hasNext() && ! st.stop) {
185            if (i >= maxSize) {
186               limit(sb, "size", i == 0, false, st);
187               break;
188            }
189            if (i++ > 0) sb.append(',');
190            write(sb, it.next(), depth + 1, st);
191         }
192         sb.append(']');
193      }
194   }
195
196   private static Iterator<Object> arrayIterator(Object array) {
197      int n = Array.getLength(array);
198      return new Iterator<>() {
199         int i;
200         @Override public boolean hasNext() { return i < n; }
201         @Override public Object next() {
202            if (i >= n)
203               throw new NoSuchElementException();
204            return Array.get(array, i++);
205         }
206      };
207   }
208
209   private static void quote(StringBuilder sb, String s) {
210      sb.append('"');
211      for (int i = 0; i < s.length(); i++) {
212         char c = s.charAt(i);
213         switch (c) {
214            case '"': sb.append("\\\""); break;
215            case '\\': sb.append("\\\\"); break;
216            case '\n': sb.append("\\n"); break;
217            case '\r': sb.append("\\r"); break;
218            case '\t': sb.append("\\t"); break;
219            case '\b': sb.append("\\b"); break;
220            case '\f': sb.append("\\f"); break;
221            default:
222               if (c < 0x20 || c == 0x7f || c == 0x2028 || c == 0x2029)
223                  sb.append(String.format("\\u%04x", (int)c));
224               else
225                  sb.append(c);
226         }
227      }
228      sb.append('"');
229   }
230}