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