001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.oapi; 014 015import static org.apache.juneau.common.internal.StringUtils.*; 016import static org.apache.juneau.httppart.HttpPartCollectionFormat.*; 017import static org.apache.juneau.httppart.HttpPartDataType.*; 018import static org.apache.juneau.httppart.HttpPartFormat.*; 019 020import java.io.IOException; 021import java.lang.reflect.*; 022import java.nio.charset.*; 023import java.time.temporal.*; 024import java.util.*; 025import java.util.Date; 026import java.util.function.*; 027 028import org.apache.juneau.*; 029import org.apache.juneau.collections.*; 030import org.apache.juneau.common.internal.*; 031import org.apache.juneau.httppart.*; 032import org.apache.juneau.internal.*; 033import org.apache.juneau.serializer.*; 034import org.apache.juneau.svl.*; 035import org.apache.juneau.swap.*; 036import org.apache.juneau.swaps.*; 037import org.apache.juneau.uon.*; 038 039/** 040 * Session object that lives for the duration of a single use of {@link OpenApiSerializer}. 041 * 042 * <h5 class='section'>Notes:</h5><ul> 043 * <li class='warn'>This class is not thread safe and is typically discarded after one use. 044 * </ul> 045 * 046 * <h5 class='section'>See Also:</h5><ul> 047 * <li class='link'><a class="doclink" href="../../../../index.html#jm.OpenApiDetails">OpenAPI Details</a> 048 049 * </ul> 050 */ 051public class OpenApiSerializerSession extends UonSerializerSession { 052 053 //----------------------------------------------------------------------------------------------------------------- 054 // Static 055 //----------------------------------------------------------------------------------------------------------------- 056 057 // Cache these for faster lookup 058 private static final BeanContext BC = BeanContext.DEFAULT; 059 private static final ClassMeta<byte[]> CM_ByteArray = BC.getClassMeta(byte[].class); 060 private static final ClassMeta<String[]> CM_StringArray = BC.getClassMeta(String[].class); 061 private static final ClassMeta<Calendar> CM_Calendar = BC.getClassMeta(Calendar.class); 062 private static final ClassMeta<Long> CM_Long = BC.getClassMeta(Long.class); 063 private static final ClassMeta<Integer> CM_Integer = BC.getClassMeta(Integer.class); 064 private static final ClassMeta<Double> CM_Double = BC.getClassMeta(Double.class); 065 private static final ClassMeta<Float> CM_Float = BC.getClassMeta(Float.class); 066 private static final ClassMeta<Boolean> CM_Boolean = BC.getClassMeta(Boolean.class); 067 068 private static final HttpPartSchema DEFAULT_SCHEMA = HttpPartSchema.DEFAULT; 069 070 /** 071 * Creates a new builder for this object. 072 * 073 * @param ctx The context creating this session. 074 * @return A new builder. 075 */ 076 public static Builder create(OpenApiSerializer ctx) { 077 return new Builder(ctx); 078 } 079 080 //----------------------------------------------------------------------------------------------------------------- 081 // Builder 082 //----------------------------------------------------------------------------------------------------------------- 083 084 /** 085 * Builder class. 086 */ 087 @FluentSetters 088 public static class Builder extends UonSerializerSession.Builder { 089 090 OpenApiSerializer ctx; 091 092 /** 093 * Constructor 094 * 095 * @param ctx The context creating this session. 096 */ 097 protected Builder(OpenApiSerializer ctx) { 098 super(ctx); 099 this.ctx = ctx; 100 } 101 102 @Override 103 public OpenApiSerializerSession build() { 104 return new OpenApiSerializerSession(this); 105 } 106 107 // <FluentSetters> 108 109 @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */ 110 public <T> Builder apply(Class<T> type, Consumer<T> apply) { 111 super.apply(type, apply); 112 return this; 113 } 114 115 @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */ 116 public Builder debug(Boolean value) { 117 super.debug(value); 118 return this; 119 } 120 121 @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */ 122 public Builder properties(Map<String,Object> value) { 123 super.properties(value); 124 return this; 125 } 126 127 @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */ 128 public Builder property(String key, Object value) { 129 super.property(key, value); 130 return this; 131 } 132 133 @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */ 134 public Builder unmodifiable() { 135 super.unmodifiable(); 136 return this; 137 } 138 139 @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */ 140 public Builder locale(Locale value) { 141 super.locale(value); 142 return this; 143 } 144 145 @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */ 146 public Builder localeDefault(Locale value) { 147 super.localeDefault(value); 148 return this; 149 } 150 151 @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */ 152 public Builder mediaType(MediaType value) { 153 super.mediaType(value); 154 return this; 155 } 156 157 @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */ 158 public Builder mediaTypeDefault(MediaType value) { 159 super.mediaTypeDefault(value); 160 return this; 161 } 162 163 @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */ 164 public Builder timeZone(TimeZone value) { 165 super.timeZone(value); 166 return this; 167 } 168 169 @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */ 170 public Builder timeZoneDefault(TimeZone value) { 171 super.timeZoneDefault(value); 172 return this; 173 } 174 175 @Override /* GENERATED - org.apache.juneau.serializer.SerializerSession.Builder */ 176 public Builder javaMethod(Method value) { 177 super.javaMethod(value); 178 return this; 179 } 180 181 @Override /* GENERATED - org.apache.juneau.serializer.SerializerSession.Builder */ 182 public Builder resolver(VarResolverSession value) { 183 super.resolver(value); 184 return this; 185 } 186 187 @Override /* GENERATED - org.apache.juneau.serializer.SerializerSession.Builder */ 188 public Builder schema(HttpPartSchema value) { 189 super.schema(value); 190 return this; 191 } 192 193 @Override /* GENERATED - org.apache.juneau.serializer.SerializerSession.Builder */ 194 public Builder schemaDefault(HttpPartSchema value) { 195 super.schemaDefault(value); 196 return this; 197 } 198 199 @Override /* GENERATED - org.apache.juneau.serializer.SerializerSession.Builder */ 200 public Builder uriContext(UriContext value) { 201 super.uriContext(value); 202 return this; 203 } 204 205 @Override /* GENERATED - org.apache.juneau.serializer.WriterSerializerSession.Builder */ 206 public Builder fileCharset(Charset value) { 207 super.fileCharset(value); 208 return this; 209 } 210 211 @Override /* GENERATED - org.apache.juneau.serializer.WriterSerializerSession.Builder */ 212 public Builder streamCharset(Charset value) { 213 super.streamCharset(value); 214 return this; 215 } 216 217 @Override /* GENERATED - org.apache.juneau.serializer.WriterSerializerSession.Builder */ 218 public Builder useWhitespace(Boolean value) { 219 super.useWhitespace(value); 220 return this; 221 } 222 223 @Override /* GENERATED - org.apache.juneau.uon.UonSerializerSession.Builder */ 224 public Builder encoding(boolean value) { 225 super.encoding(value); 226 return this; 227 } 228 229 // </FluentSetters> 230 } 231 232 //----------------------------------------------------------------------------------------------------------------- 233 // Instance 234 //----------------------------------------------------------------------------------------------------------------- 235 236 private final OpenApiSerializer ctx; 237 238 /** 239 * Constructor. 240 * 241 * @param builder The builder for this object. 242 */ 243 protected OpenApiSerializerSession(Builder builder) { 244 super(builder.encoding(false)); 245 ctx = builder.ctx; 246 } 247 248 @Override /* Serializer */ 249 protected void doSerialize(SerializerPipe out, Object o) throws IOException, SerializeException { 250 try { 251 out.getWriter().write(serialize(HttpPartType.BODY, getSchema(), o)); 252 } catch (SchemaValidationException e) { 253 throw new SerializeException(e); 254 } 255 } 256 257 @SuppressWarnings("rawtypes") 258 @Override /* PartSerializer */ 259 public String serialize(HttpPartType partType, HttpPartSchema schema, Object value) throws SerializeException, SchemaValidationException { 260 261 ClassMeta<?> type = getClassMetaForObject(value); 262 if (type == null) 263 type = object(); 264 265 // Swap if necessary 266 ObjectSwap swap = type.getSwap(this); 267 if (swap != null && ! type.isDateOrCalendarOrTemporal()) { 268 value = swap(swap, value); 269 type = swap.getSwapClassMeta(this); 270 271 // If the getSwapClass() method returns Object, we need to figure out 272 // the actual type now. 273 if (type.isObject()) 274 type = getClassMetaForObject(value); 275 } 276 277 schema = ObjectUtils.firstNonNull(schema, DEFAULT_SCHEMA); 278 279 HttpPartDataType t = schema.getType(type); 280 281 HttpPartFormat f = schema.getFormat(type); 282 if (f == HttpPartFormat.NO_FORMAT) 283 f = ctx.getFormat(); 284 285 HttpPartCollectionFormat cf = schema.getCollectionFormat(); 286 if (cf == HttpPartCollectionFormat.NO_COLLECTION_FORMAT) 287 cf = ctx.getCollectionFormat(); 288 289 String out = null; 290 291 schema.validateOutput(value, ctx.getBeanContext()); 292 293 if (type.hasMutaterTo(schema.getParsedType()) || schema.getParsedType().hasMutaterFrom(type)) { 294 value = toType(value, schema.getParsedType()); 295 type = schema.getParsedType(); 296 } 297 298 if (type.isUri()) { 299 value = getUriResolver().resolve(value); 300 type = string(); 301 } 302 303 if (value != null) { 304 305 if (t == STRING) { 306 307 if (f == BYTE) { 308 out = base64Encode(toType(value, CM_ByteArray)); 309 } else if (f == BINARY) { 310 out = toHex(toType(value, CM_ByteArray)); 311 } else if (f == BINARY_SPACED) { 312 out = toSpacedHex(toType(value, CM_ByteArray)); 313 } else if (f == DATE) { 314 try { 315 if (value instanceof Calendar) 316 out = TemporalCalendarSwap.IsoDate.DEFAULT.swap(this, (Calendar)value); 317 else if (value instanceof Date) 318 out = TemporalDateSwap.IsoDate.DEFAULT.swap(this, (Date)value); 319 else if (value instanceof Temporal) 320 out = TemporalSwap.IsoDate.DEFAULT.swap(this, (Temporal)value); 321 else 322 out = value.toString(); 323 } catch (Exception e) { 324 throw new SerializeException(e); 325 } 326 } else if (f == DATE_TIME) { 327 try { 328 if (value instanceof Calendar) 329 out = TemporalCalendarSwap.IsoInstant.DEFAULT.swap(this, (Calendar)value); 330 else if (value instanceof Date) 331 out = TemporalDateSwap.IsoInstant.DEFAULT.swap(this, (Date)value); 332 else if (value instanceof Temporal) 333 out = TemporalSwap.IsoInstant.DEFAULT.swap(this, (Temporal)value); 334 else 335 out = value.toString(); 336 } catch (Exception e) { 337 throw new SerializeException(e); 338 } 339 } else if (f == HttpPartFormat.UON) { 340 out = super.serialize(partType, schema, value); 341 } else { 342 out = toType(value, string()); 343 } 344 345 } else if (t == BOOLEAN) { 346 347 out = stringify(toType(value, CM_Boolean)); 348 349 } else if (t == INTEGER) { 350 351 if (f == INT64) 352 out = stringify(toType(value, CM_Long)); 353 else 354 out = stringify(toType(value, CM_Integer)); 355 356 } else if (t == NUMBER) { 357 358 if (f == DOUBLE) 359 out = stringify(toType(value, CM_Double)); 360 else 361 out = stringify(toType(value, CM_Float)); 362 363 } else if (t == ARRAY) { 364 365 if (cf == HttpPartCollectionFormat.UONC) 366 out = super.serialize(partType, null, toList(partType, type, value, schema)); 367 else { 368 369 HttpPartSchema items = schema.getItems(); 370 ClassMeta<?> vt = getClassMetaForObject(value); 371 OapiStringBuilder sb = new OapiStringBuilder(cf); 372 373 if (type.isArray()) { 374 for (int i = 0; i < Array.getLength(value); i++) 375 sb.append(serialize(partType, items, Array.get(value, i))); 376 } else if (type.isCollection()) { 377 ((Collection<?>)value).forEach(x -> sb.append(serialize(partType, items, x))); 378 } else if (vt.hasMutaterTo(String[].class)) { 379 String[] ss = toType(value, CM_StringArray); 380 for (int i = 0; i < ss.length; i++) 381 sb.append(serialize(partType, items, ss[i])); 382 } else { 383 throw new SerializeException("Input is not a valid array type: " + type); 384 } 385 386 out = sb.toString(); 387 } 388 389 } else if (t == OBJECT) { 390 391 if (cf == HttpPartCollectionFormat.UONC) { 392 if (schema.hasProperties() && type.isMapOrBean()) 393 value = toMap(partType, type, value, schema); 394 out = super.serialize(partType, null, value); 395 396 } else if (type.isBean()) { 397 OapiStringBuilder sb = new OapiStringBuilder(cf); 398 Predicate<Object> checkNull = x -> isKeepNullProperties() || x != null; 399 HttpPartSchema schema2 = schema; 400 401 toBeanMap(value).forEachValue(checkNull, (pMeta,key,val,thrown) -> { 402 if (thrown == null) 403 sb.append(key, serialize(partType, schema2.getProperty(key), val)); 404 }); 405 out = sb.toString(); 406 407 } else if (type.isMap()) { 408 OapiStringBuilder sb = new OapiStringBuilder(cf); 409 HttpPartSchema schema2 = schema; 410 ((Map<?,?>)value).forEach((k,v) -> sb.append(k, serialize(partType, schema2.getProperty(stringify(k)), v))); 411 out = sb.toString(); 412 413 } else { 414 throw new SerializeException("Input is not a valid object type: " + type); 415 } 416 417 } else if (t == FILE) { 418 throw new SerializeException("File part not supported."); 419 420 } else if (t == NO_TYPE) { 421 // This should never be returned by HttpPartSchema.getType(ClassMeta). 422 throw new SerializeException("Invalid type."); 423 } 424 } 425 426 schema.validateInput(out); 427 if (out == null) 428 out = schema.getDefault(); 429 if (out == null) 430 out = "null"; 431 return out; 432 } 433 434 private static class OapiStringBuilder { 435 static final AsciiSet EQ = AsciiSet.create("=\\"); 436 static final AsciiSet PIPE = AsciiSet.create("|\\"); 437 static final AsciiSet PIPE_OR_EQ = AsciiSet.create("|=\\"); 438 static final AsciiSet COMMA = AsciiSet.create(",\\"); 439 static final AsciiSet COMMA_OR_EQ = AsciiSet.create(",=\\"); 440 441 private final StringBuilder sb = new StringBuilder(); 442 private final HttpPartCollectionFormat cf; 443 private boolean first = true; 444 445 OapiStringBuilder(HttpPartCollectionFormat cf) { 446 this.cf = cf; 447 } 448 449 private void delim(HttpPartCollectionFormat cf) { 450 if (cf == PIPES) 451 sb.append('|'); 452 else if (cf == SSV) 453 sb.append(' '); 454 else if (cf == TSV) 455 sb.append('\t'); 456 else 457 sb.append(','); 458 } 459 460 OapiStringBuilder append(Object o) { 461 if (! first) 462 delim(cf); 463 first = false; 464 if (cf == PIPES) 465 sb.append(escapeChars(stringify(o), PIPE)); 466 else if (cf == SSV || cf == TSV) 467 sb.append(stringify(o)); 468 else 469 sb.append(escapeChars(stringify(o), COMMA)); 470 return this; 471 } 472 473 OapiStringBuilder append(Object key, Object val) { 474 if (! first) 475 delim(cf); 476 first = false; 477 if (cf == PIPES) 478 sb.append(escapeChars(stringify(key), PIPE_OR_EQ)).append('=').append(escapeChars(stringify(val), PIPE_OR_EQ)); 479 else if (cf == SSV || cf == TSV) 480 sb.append(escapeChars(stringify(key), EQ)).append('=').append(escapeChars(stringify(val), EQ)); 481 else 482 sb.append(escapeChars(stringify(key), COMMA_OR_EQ)).append('=').append(escapeChars(stringify(val), COMMA_OR_EQ)); 483 return this; 484 } 485 486 @Override 487 public String toString() { 488 return sb.toString(); 489 } 490 } 491 492 private Map<String,Object> toMap(HttpPartType partType, ClassMeta<?> type, Object o, HttpPartSchema s) throws SerializeException, SchemaValidationException { 493 if (s == null) 494 s = DEFAULT_SCHEMA; 495 JsonMap m = new JsonMap(); 496 if (type.isBean()) { 497 Predicate<Object> checkNull = x -> isKeepNullProperties() || x != null; 498 HttpPartSchema s2 = s; 499 toBeanMap(o).forEachValue(checkNull, (pMeta,key,val,thrown) -> { 500 if (thrown == null) 501 m.put(key, toObject(partType, val, s2.getProperty(key))); 502 }); 503 } else { 504 HttpPartSchema s2 = s; 505 ((Map<?,?>)o).forEach((k,v) -> m.put(stringify(k), toObject(partType, v, s2.getProperty(stringify(k))))); 506 } 507 if (isSortMaps()) 508 return sort(m); 509 return m; 510 } 511 512 @SuppressWarnings("rawtypes") 513 private List toList(HttpPartType partType, ClassMeta<?> type, Object o, HttpPartSchema s) throws SerializeException, SchemaValidationException { 514 if (s == null) 515 s = DEFAULT_SCHEMA; 516 JsonList l = new JsonList(); 517 HttpPartSchema items = s.getItems(); 518 if (type.isArray()) { 519 for (int i = 0; i < Array.getLength(o); i++) 520 l.add(toObject(partType, Array.get(o, i), items)); 521 } else if (type.isCollection()) { 522 ((Collection<?>)o).forEach(x -> l.add(toObject(partType, x, items))); 523 } else { 524 l.add(toObject(partType, o, items)); 525 } 526 if (isSortCollections()) 527 return sort(l); 528 return l; 529 } 530 531 @SuppressWarnings("rawtypes") 532 private Object toObject(HttpPartType partType, Object o, HttpPartSchema s) throws SerializeException, SchemaValidationException { 533 if (o == null) 534 return null; 535 if (s == null) 536 s = DEFAULT_SCHEMA; 537 ClassMeta cm = getClassMetaForObject(o); 538 HttpPartDataType t = s.getType(cm); 539 HttpPartFormat f = s.getFormat(cm); 540 HttpPartCollectionFormat cf = s.getCollectionFormat(); 541 542 if (t == STRING) { 543 if (f == BYTE) 544 return base64Encode(toType(o, CM_ByteArray)); 545 if (f == BINARY) 546 return toHex(toType(o, CM_ByteArray)); 547 if (f == BINARY_SPACED) 548 return toSpacedHex(toType(o, CM_ByteArray)); 549 if (f == DATE) 550 return toIsoDate(toType(o, CM_Calendar)); 551 if (f == DATE_TIME) 552 return toIsoDateTime(toType(o, CM_Calendar)); 553 return o; 554 } else if (t == ARRAY) { 555 List l = toList(partType, getClassMetaForObject(o), o, s); 556 if (cf == CSV) 557 return joine(l, ','); 558 if (cf == PIPES) 559 return joine(l, '|'); 560 if (cf == SSV) 561 return join(l, ' '); 562 if (cf == TSV) 563 return join(l, '\t'); 564 return l; 565 } else if (t == OBJECT) { 566 return toMap(partType, getClassMetaForObject(o), o, s); 567 } 568 569 return o; 570 } 571 572 private <T> T toType(Object in, ClassMeta<T> type) throws SerializeException { 573 try { 574 return convertToType(in, type); 575 } catch (InvalidDataConversionException e) { 576 throw new SerializeException(e); 577 } 578 } 579}