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