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}