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.marshall.oapi;
018
019import static org.apache.juneau.commons.httppart.HttpPartCollectionFormat.*;
020import static org.apache.juneau.commons.httppart.HttpPartDataType.*;
021import static org.apache.juneau.commons.httppart.HttpPartFormat.*;
022import static org.apache.juneau.commons.utils.ObjectUtils.*;
023import static org.apache.juneau.commons.utils.Shorts.*;
024import static org.apache.juneau.commons.utils.StringUtils.*;
025
026import java.io.*;
027import java.lang.reflect.*;
028import java.util.*;
029
030import org.apache.juneau.commons.httppart.*;
031import org.apache.juneau.commons.reflect.*;
032import org.apache.juneau.commons.time.*;
033import org.apache.juneau.commons.utils.*;
034import org.apache.juneau.marshall.*;
035import org.apache.juneau.marshall.collections.*;
036import org.apache.juneau.marshall.httppart.*;
037import org.apache.juneau.marshall.parser.*;
038import org.apache.juneau.marshall.swap.spi.*;
039import org.apache.juneau.marshall.uon.*;
040import org.apache.juneau.marshall.utils.*;
041
042/**
043 * Session object that lives for the duration of a single use of {@link OpenApiParser}.
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/OpenApiSupport">OpenApi</a>
051
052 * </ul>
053 */
054@SuppressWarnings({
055   "java:S110", // Inheritance depth acceptable for OpenApiParserSession hierarchy
056   "unchecked" // (T) casts of parsed part values and (Map<String,Object>) cast of type.newInstance() in the OBJECT part path follow the requested ClassMeta
057})
058public class OpenApiParserSession extends UonParserSession {
059
060   /**
061    * Builder class.
062    */
063   public static class Builder extends UonParserSession.Builder<Builder> {
064
065      private final OpenApiParser ctx;
066
067      /**
068       * Constructor
069       *
070       * @param ctx The context creating this session.
071       */
072      protected Builder(OpenApiParser ctx) {
073         super(ctx);
074         this.ctx = ctx;
075      }
076
077      @Override
078      public OpenApiParserSession build() {
079         return new OpenApiParserSession(this);
080      }
081
082   }
083
084   // Cache these for faster lookup
085   private static final MarshallingContext BC = MarshallingContext.DEFAULT;
086   private static final ClassMeta<Long> CM_Long = BC.getClassMeta(Long.class);
087   private static final ClassMeta<Integer> CM_Integer = BC.getClassMeta(Integer.class);
088   private static final ClassMeta<Double> CM_Double = BC.getClassMeta(Double.class);
089   private static final ClassMeta<Float> CM_Float = BC.getClassMeta(Float.class);
090   private static final ClassMeta<Boolean> CM_Boolean = BC.getClassMeta(Boolean.class);
091   private static final ClassMeta<JsonList> CM_JsonList = BC.getClassMeta(JsonList.class);
092
093   private static final ClassMeta<JsonMap> CM_JsonMap = BC.getClassMeta(JsonMap.class);
094
095   private static final HttpPartSchema DEFAULT_SCHEMA = HttpPartSchema.DEFAULT;
096
097   /**
098    * Creates a new builder for this object.
099    *
100    * @param ctx The context creating this session.
101    * @return A new builder.
102    */
103   public static Builder create(OpenApiParser ctx) {
104      return new Builder(ctx);
105   }
106
107   private final OpenApiParser ctx;
108
109   /**
110    * Constructor.
111    *
112    * @param builder The builder for this object.
113    */
114   protected OpenApiParserSession(Builder builder) {
115      super(builder);
116      ctx = builder.ctx;
117   }
118
119   @Override /* Overridden from HttpPartParser */
120   public <T> T read(HttpPartType partType, HttpPartSchema schema, String in, ClassMeta<T> type) throws ParseException, SchemaValidationException {
121      if (partType == null)
122         partType = HttpPartType.OTHER;
123
124      var isOptional = type.isOptional();
125
126      while (nn(type) && type.isOptional())
127         type = (ClassMeta<T>)type.getElementType();
128
129      if (type == null)
130         type = (ClassMeta<T>)object();
131
132      schema = coalesce(schema, getSchema(), DEFAULT_SCHEMA);
133
134      T t = readInner(partType, schema, in, type);
135      if (t == null && type.isPrimitive())
136         t = type.getPrimitiveDefault();
137      schema.validateOutput(t);
138
139      if (isOptional)
140         t = (T)o(t);
141
142      return t;
143   }
144
145   @SuppressWarnings({
146      "java:S3776", // Cognitive complexity acceptable for this specific logic
147      "java:S6541" // Single-threaded session contexts do not require synchronization
148   })
149   private <T> T readInner(HttpPartType partType, HttpPartSchema schema, String in, ClassMeta<T> type) throws SchemaValidationException, ParseException {
150      schema.validateInput(in);
151      if (in == null || eq(in, "null")) {
152         if (schema.getDefault() == null)
153            return null;
154         in = schema.getDefault();
155      } else {
156
157         var swap = (ObjectSwap<T,Object>)type.getSwap(this);
158         var builder = (BuilderSwap<T,Object>)type.getBuilderSwap(this);
159         ClassMeta<?> sType = null;
160         if (nn(builder))
161            sType = builder.getBuilderClassMeta(this);
162         else if (nn(swap))
163            sType = swap.getSwapClassMeta(this);
164         else
165            sType = type;
166
167         if (sType.isOptional())
168            return (T)o(readInner(partType, schema, in, sType.getElementType()));
169
170         var t = schema.getType(sType);
171         if (partType == null)
172            partType = HttpPartType.OTHER;
173
174         var f = schema.getFormat(sType);
175         if (f == HttpPartFormat.NO_FORMAT)
176            f = ctx.getFormat();
177
178         if (t == STRING) {
179            if (sType.isObject()) {
180               if (f == BYTE)
181                  return toType(base64Decode(in), type);
182               if (f == DATE || f == DATE_TIME) {
183                  var in2 = in;
184                  return toType(o(in).filter(x1 -> ! isBlank(x1)).map(x -> GranularZonedDateTime.of(in2).getZonedDateTime()).map(GregorianCalendar::from).orElse(null), type);
185               }
186               if (f == BINARY)
187                  return toType(fromHex(in), type);
188               if (f == BINARY_SPACED)
189                  return toType(fromSpacedHex(in), type);
190               if (f == HttpPartFormat.UON)
191                  return super.read(partType, schema, in, type);
192               return toType(in, type);
193            }
194            if (f == BYTE)
195               return toType(base64Decode(in), type);
196            if (f == DATE || f == DATE_TIME) {
197               if (type.isDateOrCalendarOrTemporal())
198                  return toType(Iso8601Utils.parse(in, type, getTimeZone()), type);
199               return toType(in, type);
200            }
201            if (f == BINARY)
202               return toType(fromHex(in), type);
203            if (f == BINARY_SPACED)
204               return toType(fromSpacedHex(in), type);
205            if (f == HttpPartFormat.UON)
206               return super.read(partType, schema, in, type);
207            return toType(in, type);
208
209         } else if (t == BOOLEAN) {
210            if (type.isObject())
211               type = (ClassMeta<T>)CM_Boolean;
212            if (type.isBoolean())
213               return super.read(partType, schema, in, type);
214            return toType(super.read(partType, schema, in, CM_Boolean), type);
215
216         } else if (t == INTEGER) {
217            if (type.isObject()) {
218               if (f == INT64)
219                  type = (ClassMeta<T>)CM_Long;
220               else
221                  type = (ClassMeta<T>)CM_Integer;
222            }
223            if (type.isNumber())
224               return super.read(partType, schema, in, type);
225            return toType(super.read(partType, schema, in, CM_Integer), type);
226
227         } else if (t == NUMBER) {
228            if (type.isObject()) {
229               if (f == DOUBLE)
230                  type = (ClassMeta<T>)CM_Double;
231               else
232                  type = (ClassMeta<T>)CM_Float;
233            }
234            if (type.isNumber())
235               return super.read(partType, schema, in, type);
236            return toType(super.read(partType, schema, in, CM_Double), type);
237
238         } else if (t == ARRAY) {
239
240            var cf = schema.getCollectionFormat();
241            if (cf == HttpPartCollectionFormat.NO_COLLECTION_FORMAT)
242               cf = ctx.getCollectionFormat();
243
244            if (cf == HttpPartCollectionFormat.UONC)
245               return super.read(partType, schema, in, type);
246
247            if (type.isObject())
248               type = (ClassMeta<T>)CM_JsonList;
249
250            var spt = getClassMeta(schema.getParsedType());
251            var eType = type.isObject() ? string() : type.getElementType();
252            if (eType == null)
253               eType = spt.getElementType();
254            if (eType == null)
255               eType = string();
256
257            var ss = new String[] {};
258
259            if (cf == MULTI)
260               ss = a(in);
261            else if (cf == CSV)
262               ss = StringUtils.splita(in, ',');
263            else if (cf == PIPES)
264               ss = StringUtils.splita(in, '|');
265            else if (cf == SSV)
266               ss = StringUtils.splitQuoted(in);
267            else if (cf == TSV)
268               ss = StringUtils.splita(in, '\t');
269            else if (cf == HttpPartCollectionFormat.UONC)
270               return super.read(partType, null, in, type);
271            else if (cf == NO_COLLECTION_FORMAT) {
272               if (firstNonWhitespaceChar(in) == '@' && lastNonWhitespaceChar(in) == ')')
273                  return super.read(partType, null, in, type);
274               ss = StringUtils.splita(in, ',');
275            }
276
277            var items = schema.getItems();
278            if (items == null)
279               items = HttpPartSchema.DEFAULT;
280            var o = Array.newInstance(eType.inner(), ss.length);
281            for (var i = 0; i < ss.length; i++)
282               Array.set(o, i, read(partType, items, ss[i], eType));
283            if (type.hasMutaterFrom(spt) || spt.hasMutaterTo(type))
284               return toType(toType(o, spt), type);
285            return toType(o, type);
286
287         } else if (t == OBJECT) {
288
289            var cf = schema.getCollectionFormat();
290            if (cf == HttpPartCollectionFormat.NO_COLLECTION_FORMAT)
291               cf = ctx.getCollectionFormat();
292
293            if (cf == HttpPartCollectionFormat.UONC)
294               return super.read(partType, schema, in, type);
295
296            if (type.isObject())
297               type = (ClassMeta<T>)CM_JsonMap;
298
299            if (! type.isMapOrBean())
300               throw new ParseException("Invalid type %s for part type OBJECT.", type);
301
302            var ss = new String[] {};
303
304            if (cf == MULTI)
305               ss = a(in);
306            else if (cf == CSV)
307               ss = StringUtils.splita(in, ',');
308            else if (cf == PIPES)
309               ss = StringUtils.splita(in, '|');
310            else if (cf == SSV)
311               ss = StringUtils.splitQuoted(in);
312            else if (cf == TSV)
313               ss = StringUtils.splita(in, '\t');
314            else if (cf == HttpPartCollectionFormat.UONC)
315               return super.read(partType, null, in, type);
316            else if (cf == NO_COLLECTION_FORMAT) {
317               if (firstNonWhitespaceChar(in) == '@' && lastNonWhitespaceChar(in) == ')')
318                  return super.read(partType, null, in, type);
319               ss = StringUtils.splita(in, ',');
320            }
321
322            if (type.isBean()) {
323               var m = ctx.getMarshallingContext().newBeanMap(type.inner());
324               for (var s : ss) {
325                  var kv = StringUtils.splita(s, '=', 2);
326                  if (kv.length != 2)
327                     throw new ParseException("Invalid input %s for part type OBJECT.", in);
328                  var key = kv[0];
329                  var value = kv[1];
330                  var bpm = m.getPropertyMeta(key);
331                  if (bpm == null && ! isIgnoreUnknownBeanProperties())
332                     throw new ParseException("Invalid input %s for part type OBJECT.  Cannot find property %s", in, key);
333                  m.put(key, read(partType, schema.getProperty(key), value, ((ClassMeta<T>)(bpm == null ? object() : bpm.getBeanInfo()))));
334               }
335               return checkRequired(m).getBean();
336            }
337
338            var eType = type.isObject() ? string() : type.getValueType();
339            if (eType == null)
340               eType = getClassMeta(schema.getParsedType()).getValueType();
341            if (eType == null)
342               eType = string();
343
344            try {
345               var m = (Map<String,Object>)type.newInstance();
346               if (m == null)
347                  m = JsonMap.create();
348
349               for (var s : ss) {
350                  var kv = StringUtils.splita(s, '=', 2);
351                  if (kv.length != 2)
352                     throw new ParseException("Invalid input %s for part type OBJECT.", in);
353                  var key = kv[0];
354                  var value = kv[1];
355                  m.put(key, read(partType, schema.getProperty(key), value, eType));
356               }
357               return (T)m;
358            } catch (ExecutableException e) {
359               throw new ParseException(e);
360            }
361
362         } else if (t == FILE) {
363            throw new ParseException("File part not supported.");
364
365         } else if (t == NO_TYPE) {
366            // This should never be returned by HttpPartSchema.getType(ClassMeta).
367            throw new ParseException("Invalid type.");
368         }
369      }
370
371      return super.read(partType, schema, in, type);
372   }
373
374   private <T> T toType(Object in, ClassMeta<T> type) throws ParseException {
375      try {
376         return convertToType(in, type);
377      } catch (InvalidDataConversionException e) {
378         throw new ParseException(e.getMessage());
379      }
380   }
381
382   @Override /* Overridden from ParserSession */
383   protected <T> T doRead(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException {
384      return readInner(null, HttpPartSchema.DEFAULT, pipe.asString(), type);
385   }
386}