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.uon; 018 019import static org.apache.juneau.commons.utils.IoUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021import static org.apache.juneau.commons.utils.ThrowableUtils.*; 022 023import java.io.*; 024import java.time.*; 025import java.time.temporal.*; 026import java.util.*; 027import java.util.function.*; 028 029import org.apache.juneau.commons.bean.*; 030import org.apache.juneau.commons.conversion.*; 031import org.apache.juneau.commons.httppart.*; 032import org.apache.juneau.commons.lang.*; 033import org.apache.juneau.marshall.*; 034import org.apache.juneau.marshall.httppart.*; 035import org.apache.juneau.marshall.serializer.*; 036import org.apache.juneau.marshall.stream.*; 037 038/** 039 * Session object that lives for the duration of a single use of {@link UonSerializer}. 040 * 041 * <h5 class='section'>Notes:</h5><ul> 042 * <li class='warn'>This class is not thread safe and is typically discarded after one use. 043 * </ul> 044 * 045 * <h5 class='section'>See Also:</h5><ul> 046 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/UonSupport">UON</a> 047 048 * </ul> 049 */ 050@SuppressWarnings({ 051 "java:S110", // Inheritance depth acceptable for this class hierarchy 052 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 053 "rawtypes", // writeCollection(), writeMap() and the Map/Collection/BeanMap casts in the dispatch use raw types because the object was already matched by runtime kind 054 "resource", // Resource management handled externally 055 "unchecked" // Type erasure requires unchecked casts in collection/map serialization 056}) 057public class UonSerializerSession extends WriterSerializerSession implements HttpPartSerializerSession, RecordWritable { 058 059 /** 060 * Builder class. 061 */ 062 @SuppressWarnings({ 063 "java:S119" // 'SELF' (CRTP self-type) is intentional and clearer than a single-letter name. 064 }) 065 public abstract static class Builder<SELF extends Builder<SELF>> extends WriterSerializerSession.Builder<SELF> { 066 067 private boolean encoding; 068 private ParamFormat paramFormat; 069 070 /** 071 * Constructor 072 * 073 * @param ctx The context creating this session. 074 * <br>Cannot be <jk>null</jk>. 075 */ 076 protected Builder(UonSerializer ctx) { 077 super(reqnn("ctx", ctx)); 078 encoding = ctx.isEncoding(); 079 paramFormat = ctx.getParamFormat(); 080 } 081 082 @Override 083 public UonSerializerSession build() { 084 return new UonSerializerSession(this); 085 } 086 087 /** 088 * Encode non-valid URI characters. 089 * 090 * @param value The new value for this setting. 091 * @return This object. 092 */ 093 public SELF encoding(boolean value) { 094 encoding = value; 095 return self(); 096 } 097 098 /** 099 * Format to use for query/form-data/header values. 100 * 101 * @param value The new value for this setting. 102 * @return This object. 103 */ 104 public SELF paramFormat(ParamFormat value) { 105 paramFormat = value; 106 return self(); 107 } 108 109 @Override /* Overridden from Builder */ 110 public SELF property(String key, Object value) { 111 if (key == null) { super.property(key, value); return self(); } 112 switch (key) { 113 case "encoding", "UonSerializerSession.encoding": 114 return encoding(cvt(value, Boolean.class)); 115 case "paramFormat", "UonSerializerSession.paramFormat": 116 return paramFormat(cvt(value, ParamFormat.class)); 117 default: 118 super.property(key, value); 119 return self(); 120 } 121 } 122 123 } 124 125 /** 126 * Concrete default builder leaf for the non-subclassed {@code create()} path (CRTP terminal). 127 */ 128 public static final class DefaultBuilder extends Builder<DefaultBuilder> { 129 130 DefaultBuilder(UonSerializer ctx) { 131 super(ctx); 132 } 133 } 134 135 /** 136 * Creates a new builder for this object. 137 * 138 * @param ctx The context creating this session. 139 * <br>Cannot be <jk>null</jk>. 140 * @return A new builder. 141 */ 142 @SuppressWarnings({ 143 "java:S1452" // Builder<?> wildcard return intentional; callers use it to construct session instances polymorphically 144 }) 145 public static Builder<?> create(UonSerializer ctx) { 146 return new DefaultBuilder(reqnn("ctx", ctx)); 147 } 148 149 private final boolean encoding; 150 private final ParamFormat paramFormat; 151 private final boolean plainTextParams; 152 153 /** 154 * Constructor. 155 * 156 * @param builder The builder for this object. 157 */ 158 public UonSerializerSession(Builder<?> builder) { 159 super(builder); 160 encoding = builder.encoding; 161 paramFormat = builder.paramFormat; 162 plainTextParams = paramFormat == ParamFormat.PLAINTEXT; 163 } 164 165 @Override /* RecordWritable */ 166 public RecordWriter writeRecords(Object output) throws IOException { 167 return RecordAdapter.writer(this, output); 168 } 169 170 @Override /* RecordWritable */ 171 public boolean isRecordStreaming() { 172 return false; 173 } 174 175 @Override /* Overridden from HttpPartSerializer */ 176 public String write(HttpPartType type, HttpPartSchema schema, Object value) throws SerializeException, SchemaValidationException { 177 try { 178 // Shortcut for simple types. 179 var cm = getClassMetaForObject(value); 180 if (nn(cm) && (schema == null || schema.getType() == HttpPartDataType.NO_TYPE)) { 181 if (cm.isNumber() || cm.isBoolean()) 182 return BasicConverter.INSTANCE.to(value, String.class); 183 if (cm.isString()) { 184 var s = BasicConverter.INSTANCE.to(value, String.class); 185 if (s.isEmpty() || ! UonUtils.needsQuotes(s)) 186 return s; 187 } 188 } 189 var w = new StringWriter(); 190 writeAnything(getUonWriter(w).i(getInitialDepth()), value, getExpectedRootType(value), "root", null); 191 return w.toString(); 192 } catch (Exception e) { 193 throw toRex(e); 194 } 195 } 196 197 private final UonWriter getUonWriter(Writer out) { 198 return new UonWriter(this, out, isUseWhitespace(), getMaxIndent(), isEncoding(), isTrimStrings(), plainTextParams, getQuoteChar(), getUriResolver()); 199 } 200 201 private SerializerWriter<?> writeBeanMap(UonWriter out, BeanMap<?> m, String typeName) throws SerializeException { 202 203 if (! plainTextParams) 204 out.append('('); 205 206 var addComma = Flag.create(); 207 208 if (nn(typeName)) { 209 var pm = m.getMeta().getTypeProperty(); 210 out.cr(indent).appendObject(pm.getName(), false).append('=').appendObject(typeName, false); 211 addComma.set(); 212 } 213 214 var checkNull = (Predicate<Object>)(x -> isKeepNullProperties() || nn(x)); 215 m.forEachValue(checkNull, (pMeta, key, value, thrown) -> { 216 var cMeta = (ClassMeta<?>) pMeta.getBeanInfo(); 217 218 if (nn(thrown)) 219 onBeanGetterException(pMeta, thrown); 220 221 if (canIgnoreValue(pMeta, key, value)) 222 return; 223 224 addComma.ifSet(() -> out.append(',')).set(); 225 226 out.cr(indent).appendObject(key, false).append('='); 227 228 writeAnything(out, value, cMeta, key, pMeta); 229 }); 230 231 if (m.size() > 0) 232 out.cre(indent - 1); 233 if (! plainTextParams) 234 out.append(')'); 235 236 return out; 237 } 238 239 private SerializerWriter<?> writeCollection(UonWriter out, Collection c, ClassMeta<?> type) throws SerializeException { 240 241 var elementType = type.getElementType(); 242 243 if (! plainTextParams) 244 out.append('@').append('('); 245 246 var addComma = Flag.create(); 247 forEachEntry(c, x -> { 248 addComma.ifSet(() -> out.append(',')).set(); 249 out.cr(indent); 250 writeAnything(out, x, elementType, "<iterator>", null); 251 }); 252 253 addComma.ifSet(() -> out.cre(indent - 1)); 254 if (! plainTextParams) 255 out.append(')'); 256 257 return out; 258 } 259 260 private SerializerWriter<?> writeStreamable(UonWriter out, Object o, ClassMeta<?> sType, ClassMeta<?> type) throws SerializeException { 261 262 var elementType = type.getElementType(); 263 264 if (! plainTextParams) 265 out.append('@').append('('); 266 267 var addComma = Flag.create(); 268 forEachStreamableEntry(o, sType, x -> { 269 addComma.ifSet(() -> out.append(',')).set(); 270 out.cr(indent); 271 writeAnything(out, x, elementType, "<iterator>", null); 272 }); 273 274 addComma.ifSet(() -> out.cre(indent - 1)); 275 if (! plainTextParams) 276 out.append(')'); 277 278 return out; 279 } 280 281 private SerializerWriter<?> writeMap(UonWriter out, Map m, ClassMeta<?> type) throws SerializeException { 282 283 var keyType = type.getKeyType(); 284 var valueType = type.getValueType(); 285 286 if (! plainTextParams) 287 out.append('('); 288 289 var addComma = Flag.create(); 290 forEachEntry(m, x -> { 291 addComma.ifSet(() -> out.append(',')).set(); 292 var value = x.getValue(); 293 var key = generalize(x.getKey(), keyType); 294 out.cr(indent).appendObject(key, false).append('='); 295 writeAnything(out, value, valueType, toString(key), null); 296 }); 297 298 addComma.ifSet(() -> out.cre(indent - 1)); 299 300 if (! plainTextParams) 301 out.append(')'); 302 303 return out; 304 } 305 306 @Override /* Overridden from Serializer */ 307 protected void doWrite(SerializerPipe out, Object o) throws IOException, SerializeException { 308 writeAnything(getUonWriter(out).i(getInitialDepth()), o, getExpectedRootType(o), "root", null); 309 } 310 311 /** 312 * Format to use for query/form-data/header values. 313 * 314 * @see UonSerializer.Builder#paramFormat(ParamFormat) 315 * @return 316 * Specifies the format to use for URL GET parameter keys and values. 317 */ 318 protected final ParamFormat getParamFormat() { return paramFormat; } 319 320 /** 321 * Converts the specified output target object to an {@link UonWriter}. 322 * 323 * @param out The output target object. 324 * <br>Must not be <jk>null</jk>. 325 * @return The output target object wrapped in an {@link UonWriter}. 326 */ 327 protected final UonWriter getUonWriter(SerializerPipe out) { 328 var output = out.getRawOutput(); 329 if (output instanceof UonWriter output2) 330 return output2; 331 var w = new UonWriter(this, out.getWriter(), isUseWhitespace(), getMaxIndent(), isEncoding(), isTrimStrings(), plainTextParams, getQuoteChar(), getUriResolver()); 332 out.setWriter(w); 333 return w; 334 } 335 336 @Override 337 public final boolean isAddBeanTypes() { return super.isAddBeanTypes(); } 338 339 /** 340 * Encode non-valid URI characters. 341 * 342 * @see UonSerializer.Builder#encoding() 343 * @return 344 * <jk>true</jk> if non-valid URI characters should be encoded with <js>"%xx"</js> constructs. 345 */ 346 protected final boolean isEncoding() { return encoding; } 347 348 /** 349 * Workhorse method. 350 * 351 * <p> 352 * Determines the type of object, and then calls the appropriate type-specific serialization method. 353 * 354 * @param out The writer to serialize to. 355 * <br>Must not be <jk>null</jk>. 356 * @param o The object being serialized. 357 * <br>Can be <jk>null</jk> (written as the UON null literal). 358 * @param eType The expected type of the object if this is a bean property. 359 * <br>Can be <jk>null</jk> (defaults to {@code Object}). 360 * @param attrName 361 * The bean property name if this is a bean property. 362 * <jk>null</jk> if this isn't a bean property being serialized. 363 * @param pMeta The bean property metadata. 364 * <br>Can be <jk>null</jk> (bean-property-level URI detection is skipped). 365 * @return The same writer passed in. 366 * @throws SerializeException Generic serialization error occurred. 367 */ 368 @SuppressWarnings({ 369 "java:S3776" // Cognitive complexity acceptable for this specific logic 370 }) 371 protected UonWriter writeAnything(UonWriter out, Object o, ClassMeta<?> eType, String attrName, BeanPropertyMeta pMeta) throws SerializeException { 372 373 if (o == null) { 374 out.appendObject(null, false); 375 return out; 376 } 377 378 if (eType == null) 379 eType = object(); 380 381 ClassMeta<?> aType = null; // The actual type 382 ClassMeta<?> sType = null; // The serialized type 383 384 aType = push2(attrName, o, eType); 385 boolean isRecursion = aType == null; 386 387 // Handle recursion 388 if (aType == null) { 389 o = null; 390 aType = object(); 391 } 392 393 // Handle Optional<X> 394 if (isOptional(aType)) { 395 o = getOptionalValue(o); 396 eType = getOptionalType(eType); 397 aType = getClassMetaForObject(o, object()); 398 } 399 400 sType = aType; 401 var typeName = getBeanTypeName(this, eType, aType, pMeta); 402 403 // Swap if necessary 404 var swap = aType.getSwap(this); 405 if (nn(swap)) { 406 o = swap(swap, o); 407 sType = swap.getSwapClassMeta(this); 408 409 // If the getSwapClass() method returns Object, we need to figure out 410 // the actual type now. 411 if (sType.isObject()) 412 sType = getClassMetaForObject(o); 413 } 414 415 // '\0' characters are considered null. 416 if (o == null || (sType.isChar() && ((Character)o).charValue() == 0)) 417 out.appendObject(null, false); 418 else if (sType.isBean()) 419 writeBeanMap(out, toBeanMap(o), typeName); 420 else if (sType.isMap()) { 421 if (sType.isBeanMap()) 422 writeBeanMap(out, (BeanMap)o, typeName); 423 else 424 writeMap(out, (Map)o, eType); 425 } else if (sType.isCollection()) { 426 writeCollection(out, (Collection)o, eType); 427 } else if (sType.isArray()) { 428 writeCollection(out, toList(sType.inner(), o), eType); 429 } else if (sType.isBoolean()) { 430 out.appendBoolean(o); 431 } else if (sType.isNumber()) { 432 out.appendNumber(o); 433 } else if (sType.isUri() || (nn(pMeta) && pMeta.isUri())) { 434 out.appendUri(o); 435 } else if (sType.isDate()) { 436 out.appendObject(writeDate((Date)o, sType), false); 437 } else if (sType.isCalendar()) { 438 out.appendObject(writeCalendar(o, sType), false); 439 } else if (sType.isTemporal()) { 440 out.appendObject(writeTemporal((TemporalAccessor)o, sType), false); 441 } else if (sType.isDuration()) { 442 out.appendObject(writeDuration((Duration)o), false); 443 } else if (sType.isPeriod()) { 444 out.appendObject(writePeriod((Period)o), false); 445 } else if (sType.isStreamable()) { 446 writeStreamable(out, o, sType, eType); 447 } else if (sType.isReader()) { 448 pipe((Reader)o, out, SerializerSession::handleThrown); 449 } else if (sType.isInputStream()) { 450 pipe((InputStream)o, out, SerializerSession::handleThrown); 451 } else { 452 out.appendObject(o, false); 453 } 454 455 if (! isRecursion) 456 pop(); 457 return out; 458 } 459}