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