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}