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