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.CollectionUtils.*; 020import static org.apache.juneau.commons.utils.StringUtils.*; 021import static org.apache.juneau.commons.utils.Utils.*; 022import static org.apache.juneau.httppart.HttpPartCollectionFormat.*; 023import static org.apache.juneau.httppart.HttpPartDataType.*; 024import static org.apache.juneau.httppart.HttpPartFormat.*; 025 026import java.io.*; 027import java.lang.reflect.*; 028import java.nio.charset.*; 029import java.util.*; 030import java.util.function.*; 031 032import org.apache.juneau.*; 033import org.apache.juneau.collections.*; 034import org.apache.juneau.commons.reflect.*; 035import org.apache.juneau.commons.time.*; 036import org.apache.juneau.commons.utils.*; 037import org.apache.juneau.httppart.*; 038import org.apache.juneau.parser.*; 039import org.apache.juneau.swap.*; 040import org.apache.juneau.swaps.*; 041import org.apache.juneau.uon.*; 042 043/** 044 * Session object that lives for the duration of a single use of {@link OpenApiParser}. 045 * 046 * <h5 class='section'>Notes:</h5><ul> 047 * <li class='warn'>This class is not thread safe and is typically discarded after one use. 048 * </ul> 049 * 050 * <h5 class='section'>See Also:</h5><ul> 051 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/OpenApiBasics">OpenApi Basics</a> 052 053 * </ul> 054 */ 055public class OpenApiParserSession extends UonParserSession { 056 /** 057 * Builder class. 058 */ 059 public static class Builder extends UonParserSession.Builder { 060 061 private OpenApiParser ctx; 062 063 /** 064 * Constructor 065 * 066 * @param ctx The context creating this session. 067 */ 068 protected Builder(OpenApiParser 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 OpenApiParserSession build() { 081 return new OpenApiParserSession(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 decoding(boolean value) { 092 super.decoding(value); 093 return this; 094 } 095 096 @Override /* Overridden from Builder */ 097 public Builder fileCharset(Charset value) { 098 super.fileCharset(value); 099 return this; 100 } 101 102 @Override /* Overridden from Builder */ 103 public Builder javaMethod(Method value) { 104 super.javaMethod(value); 105 return this; 106 } 107 108 @Override /* Overridden from Builder */ 109 public Builder locale(Locale value) { 110 super.locale(value); 111 return this; 112 } 113 114 @Override /* Overridden from Builder */ 115 public Builder mediaType(MediaType value) { 116 super.mediaType(value); 117 return this; 118 } 119 120 @Override /* Overridden from Builder */ 121 public Builder mediaTypeDefault(MediaType value) { 122 super.mediaTypeDefault(value); 123 return this; 124 } 125 126 @Override /* Overridden from Builder */ 127 public Builder outer(Object value) { 128 super.outer(value); 129 return this; 130 } 131 132 @Override /* Overridden from Builder */ 133 public Builder properties(Map<String,Object> value) { 134 super.properties(value); 135 return this; 136 } 137 138 @Override /* Overridden from Builder */ 139 public Builder property(String key, Object value) { 140 super.property(key, value); 141 return this; 142 } 143 144 @Override /* Overridden from Builder */ 145 public Builder schema(HttpPartSchema value) { 146 super.schema(value); 147 return this; 148 } 149 150 @Override /* Overridden from Builder */ 151 public Builder schemaDefault(HttpPartSchema value) { 152 super.schemaDefault(value); 153 return this; 154 } 155 156 @Override /* Overridden from Builder */ 157 public Builder streamCharset(Charset value) { 158 super.streamCharset(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 unmodifiable() { 176 super.unmodifiable(); 177 return this; 178 } 179 } 180 181 // Cache these for faster lookup 182 private static final BeanContext BC = BeanContext.DEFAULT; 183 private static final ClassMeta<Long> CM_Long = BC.getClassMeta(Long.class); 184 private static final ClassMeta<Integer> CM_Integer = BC.getClassMeta(Integer.class); 185 private static final ClassMeta<Double> CM_Double = BC.getClassMeta(Double.class); 186 private static final ClassMeta<Float> CM_Float = BC.getClassMeta(Float.class); 187 private static final ClassMeta<Boolean> CM_Boolean = BC.getClassMeta(Boolean.class); 188 private static final ClassMeta<JsonList> CM_JsonList = BC.getClassMeta(JsonList.class); 189 190 private static final ClassMeta<JsonMap> CM_JsonMap = BC.getClassMeta(JsonMap.class); 191 192 private static final HttpPartSchema DEFAULT_SCHEMA = HttpPartSchema.DEFAULT; 193 194 /** 195 * Creates a new builder for this object. 196 * 197 * @param ctx The context creating this session. 198 * @return A new builder. 199 */ 200 public static Builder create(OpenApiParser ctx) { 201 return new Builder(ctx); 202 } 203 204 private final OpenApiParser ctx; 205 206 /** 207 * Constructor. 208 * 209 * @param builder The builder for this object. 210 */ 211 protected OpenApiParserSession(Builder builder) { 212 super(builder); 213 ctx = builder.ctx; 214 } 215 216 @SuppressWarnings("unchecked") 217 @Override /* Overridden from HttpPartParser */ 218 public <T> T parse(HttpPartType partType, HttpPartSchema schema, String in, ClassMeta<T> type) throws ParseException, SchemaValidationException { 219 if (partType == null) 220 partType = HttpPartType.OTHER; 221 222 var isOptional = type.isOptional(); 223 224 while (nn(type) && type.isOptional()) 225 type = (ClassMeta<T>)type.getElementType(); 226 227 if (type == null) 228 type = (ClassMeta<T>)object(); 229 230 schema = firstNonNull(schema, getSchema(), DEFAULT_SCHEMA); 231 232 T t = parseInner(partType, schema, in, type); 233 if (t == null && type.isPrimitive()) 234 t = type.getPrimitiveDefault(); 235 schema.validateOutput(t, ctx.getBeanContext()); 236 237 if (isOptional) 238 t = (T)opt(t); 239 240 return t; 241 } 242 243 @SuppressWarnings({ "unchecked" }) 244 private <T> T parseInner(HttpPartType partType, HttpPartSchema schema, String in, ClassMeta<T> type) throws SchemaValidationException, ParseException { 245 schema.validateInput(in); 246 if (in == null || "null".equals(in)) { 247 if (schema.getDefault() == null) 248 return null; 249 in = schema.getDefault(); 250 } else { 251 252 var swap = (ObjectSwap<T,Object>)type.getSwap(this); 253 var builder = (BuilderSwap<T,Object>)type.getBuilderSwap(this); 254 var sType = (ClassMeta<?>)null; 255 if (nn(builder)) 256 sType = builder.getBuilderClassMeta(this); 257 else if (nn(swap)) 258 sType = swap.getSwapClassMeta(this); 259 else 260 sType = type; 261 262 if (sType.isOptional()) 263 return (T)opt(parseInner(partType, schema, in, sType.getElementType())); 264 265 var t = schema.getType(sType); 266 if (partType == null) 267 partType = HttpPartType.OTHER; 268 269 var f = schema.getFormat(sType); 270 if (f == HttpPartFormat.NO_FORMAT) 271 f = ctx.getFormat(); 272 273 if (t == STRING) { 274 if (sType.isObject()) { 275 if (f == BYTE) 276 return toType(base64Decode(in), type); 277 if (f == DATE || f == DATE_TIME) { 278 var in2 = in; 279 return toType(opt(in).filter(x1 -> ! isBlank(x1)).map(x -> GranularZonedDateTime.of(in2).getZonedDateTime()).map(GregorianCalendar::from).orElse(null), type); 280 } 281 if (f == BINARY) 282 return toType(fromHex(in), type); 283 if (f == BINARY_SPACED) 284 return toType(fromSpacedHex(in), type); 285 if (f == HttpPartFormat.UON) 286 return super.parse(partType, schema, in, type); 287 return toType(in, type); 288 } 289 if (f == BYTE) 290 return toType(base64Decode(in), type); 291 if (f == DATE) { 292 try { 293 if (type.isCalendar()) 294 return toType(TemporalCalendarSwap.IsoDate.DEFAULT.unswap(this, in, type), type); 295 if (type.isDate()) 296 return toType(TemporalDateSwap.IsoDate.DEFAULT.unswap(this, in, type), type); 297 if (type.isTemporal()) 298 return toType(TemporalSwap.IsoDate.DEFAULT.unswap(this, in, type), type); 299 return toType(in, type); 300 } catch (Exception e) { 301 throw new ParseException(e); 302 } 303 } 304 if (f == DATE_TIME) { 305 try { 306 if (type.isCalendar()) 307 return toType(TemporalCalendarSwap.IsoDateTime.DEFAULT.unswap(this, in, type), type); 308 if (type.isDate()) 309 return toType(TemporalDateSwap.IsoDateTime.DEFAULT.unswap(this, in, type), type); 310 if (type.isTemporal()) 311 return toType(TemporalSwap.IsoDateTime.DEFAULT.unswap(this, in, type), type); 312 return toType(in, type); 313 } catch (Exception e) { 314 throw new ParseException(e); 315 } 316 } 317 if (f == BINARY) 318 return toType(fromHex(in), type); 319 if (f == BINARY_SPACED) 320 return toType(fromSpacedHex(in), type); 321 if (f == HttpPartFormat.UON) 322 return super.parse(partType, schema, in, type); 323 return toType(in, type); 324 325 } else if (t == BOOLEAN) { 326 if (type.isObject()) 327 type = (ClassMeta<T>)CM_Boolean; 328 if (type.isBoolean()) 329 return super.parse(partType, schema, in, type); 330 return toType(super.parse(partType, schema, in, CM_Boolean), type); 331 332 } else if (t == INTEGER) { 333 if (type.isObject()) { 334 if (f == INT64) 335 type = (ClassMeta<T>)CM_Long; 336 else 337 type = (ClassMeta<T>)CM_Integer; 338 } 339 if (type.isNumber()) 340 return super.parse(partType, schema, in, type); 341 return toType(super.parse(partType, schema, in, CM_Integer), type); 342 343 } else if (t == NUMBER) { 344 if (type.isObject()) { 345 if (f == DOUBLE) 346 type = (ClassMeta<T>)CM_Double; 347 else 348 type = (ClassMeta<T>)CM_Float; 349 } 350 if (type.isNumber()) 351 return super.parse(partType, schema, in, type); 352 return toType(super.parse(partType, schema, in, CM_Double), type); 353 354 } else if (t == ARRAY) { 355 356 var cf = schema.getCollectionFormat(); 357 if (cf == HttpPartCollectionFormat.NO_COLLECTION_FORMAT) 358 cf = ctx.getCollectionFormat(); 359 360 if (cf == HttpPartCollectionFormat.UONC) 361 return super.parse(partType, schema, in, type); 362 363 if (type.isObject()) 364 type = (ClassMeta<T>)CM_JsonList; 365 366 var eType = type.isObject() ? string() : type.getElementType(); 367 if (eType == null) 368 eType = schema.getParsedType().getElementType(); 369 if (eType == null) 370 eType = string(); 371 372 var ss = new String[] {}; 373 374 if (cf == MULTI) 375 ss = a(in); 376 else if (cf == CSV) 377 ss = StringUtils.splita(in, ','); 378 else if (cf == PIPES) 379 ss = StringUtils.splita(in, '|'); 380 else if (cf == SSV) 381 ss = StringUtils.splitQuoted(in); 382 else if (cf == TSV) 383 ss = StringUtils.splita(in, '\t'); 384 else if (cf == HttpPartCollectionFormat.UONC) 385 return super.parse(partType, null, in, type); 386 else if (cf == NO_COLLECTION_FORMAT) { 387 if (firstNonWhitespaceChar(in) == '@' && lastNonWhitespaceChar(in) == ')') 388 return super.parse(partType, null, in, type); 389 ss = StringUtils.splita(in, ','); 390 } 391 392 var items = schema.getItems(); 393 if (items == null) 394 items = HttpPartSchema.DEFAULT; 395 var o = Array.newInstance(eType.inner(), ss.length); 396 for (var i = 0; i < ss.length; i++) 397 Array.set(o, i, parse(partType, items, ss[i], eType)); 398 if (type.hasMutaterFrom(schema.getParsedType()) || schema.getParsedType().hasMutaterTo(type)) 399 return toType(toType(o, schema.getParsedType()), type); 400 return toType(o, type); 401 402 } else if (t == OBJECT) { 403 404 var cf = schema.getCollectionFormat(); 405 if (cf == HttpPartCollectionFormat.NO_COLLECTION_FORMAT) 406 cf = ctx.getCollectionFormat(); 407 408 if (cf == HttpPartCollectionFormat.UONC) 409 return super.parse(partType, schema, in, type); 410 411 if (type.isObject()) 412 type = (ClassMeta<T>)CM_JsonMap; 413 414 if (! type.isMapOrBean()) 415 throw new ParseException("Invalid type {0} for part type OBJECT.", type); 416 417 var ss = new String[] {}; 418 419 if (cf == MULTI) 420 ss = a(in); 421 else if (cf == CSV) 422 ss = StringUtils.splita(in, ','); 423 else if (cf == PIPES) 424 ss = StringUtils.splita(in, '|'); 425 else if (cf == SSV) 426 ss = StringUtils.splitQuoted(in); 427 else if (cf == TSV) 428 ss = StringUtils.splita(in, '\t'); 429 else if (cf == HttpPartCollectionFormat.UONC) 430 return super.parse(partType, null, in, type); 431 else if (cf == NO_COLLECTION_FORMAT) { 432 if (firstNonWhitespaceChar(in) == '@' && lastNonWhitespaceChar(in) == ')') 433 return super.parse(partType, null, in, type); 434 ss = StringUtils.splita(in, ','); 435 } 436 437 if (type.isBean()) { 438 var m = ctx.getBeanContext().newBeanMap(type.inner()); 439 for (var s : ss) { 440 var kv = StringUtils.splita(s, '=', 2); 441 if (kv.length != 2) 442 throw new ParseException("Invalid input {0} for part type OBJECT.", in); 443 var key = kv[0]; 444 var value = kv[1]; 445 var bpm = m.getPropertyMeta(key); 446 if (bpm == null && ! isIgnoreUnknownBeanProperties()) 447 throw new ParseException("Invalid input {0} for part type OBJECT. Cannot find property {1}", in, key); 448 m.put(key, parse(partType, schema.getProperty(key), value, ((ClassMeta<T>)(bpm == null ? object() : bpm.getClassMeta())))); 449 } 450 return m.getBean(); 451 } 452 453 var eType = type.isObject() ? string() : type.getValueType(); 454 if (eType == null) 455 eType = schema.getParsedType().getValueType(); 456 if (eType == null) 457 eType = string(); 458 459 try { 460 var m = (Map<String,Object>)type.newInstance(); 461 if (m == null) 462 m = JsonMap.create(); 463 464 for (var s : ss) { 465 var kv = StringUtils.splita(s, '=', 2); 466 if (kv.length != 2) 467 throw new ParseException("Invalid input {0} for part type OBJECT.", in); 468 var key = kv[0]; 469 var value = kv[1]; 470 m.put(key, parse(partType, schema.getProperty(key), value, eType)); 471 } 472 return (T)m; 473 } catch (ExecutableException e) { 474 throw new ParseException(e); 475 } 476 477 } else if (t == FILE) { 478 throw new ParseException("File part not supported."); 479 480 } else if (t == NO_TYPE) { 481 // This should never be returned by HttpPartSchema.getType(ClassMeta). 482 throw new ParseException("Invalid type."); 483 } 484 } 485 486 return super.parse(partType, schema, in, type); 487 } 488 489 private <T> T toType(Object in, ClassMeta<T> type) throws ParseException { 490 try { 491 return convertToType(in, type); 492 } catch (InvalidDataConversionException e) { 493 throw new ParseException(e.getMessage()); 494 } 495 } 496 497 @Override /* Overridden from ParserSession */ 498 protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException { 499 return parseInner(null, HttpPartSchema.DEFAULT, pipe.asString(), type); 500 } 501}