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}