001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              *
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.oapi;
014
015import static org.apache.juneau.common.internal.StringUtils.*;
016import static org.apache.juneau.httppart.HttpPartCollectionFormat.*;
017import static org.apache.juneau.httppart.HttpPartDataType.*;
018import static org.apache.juneau.httppart.HttpPartFormat.*;
019
020import java.io.IOException;
021import java.lang.reflect.*;
022import java.nio.charset.*;
023import java.time.temporal.*;
024import java.util.*;
025import java.util.Date;
026import java.util.function.*;
027
028import org.apache.juneau.*;
029import org.apache.juneau.collections.*;
030import org.apache.juneau.common.internal.*;
031import org.apache.juneau.httppart.*;
032import org.apache.juneau.internal.*;
033import org.apache.juneau.serializer.*;
034import org.apache.juneau.svl.*;
035import org.apache.juneau.swap.*;
036import org.apache.juneau.swaps.*;
037import org.apache.juneau.uon.*;
038
039/**
040 * Session object that lives for the duration of a single use of {@link OpenApiSerializer}.
041 *
042 * <h5 class='section'>Notes:</h5><ul>
043 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
044 * </ul>
045 *
046 * <h5 class='section'>See Also:</h5><ul>
047 *    <li class='link'><a class="doclink" href="../../../../index.html#jm.OpenApiDetails">OpenAPI Details</a>
048
049 * </ul>
050 */
051public class OpenApiSerializerSession extends UonSerializerSession {
052
053   //-----------------------------------------------------------------------------------------------------------------
054   // Static
055   //-----------------------------------------------------------------------------------------------------------------
056
057   // Cache these for faster lookup
058   private static final BeanContext BC = BeanContext.DEFAULT;
059   private static final ClassMeta<byte[]> CM_ByteArray = BC.getClassMeta(byte[].class);
060   private static final ClassMeta<String[]> CM_StringArray = BC.getClassMeta(String[].class);
061   private static final ClassMeta<Calendar> CM_Calendar = BC.getClassMeta(Calendar.class);
062   private static final ClassMeta<Long> CM_Long = BC.getClassMeta(Long.class);
063   private static final ClassMeta<Integer> CM_Integer = BC.getClassMeta(Integer.class);
064   private static final ClassMeta<Double> CM_Double = BC.getClassMeta(Double.class);
065   private static final ClassMeta<Float> CM_Float = BC.getClassMeta(Float.class);
066   private static final ClassMeta<Boolean> CM_Boolean = BC.getClassMeta(Boolean.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(OpenApiSerializer ctx) {
077      return new Builder(ctx);
078   }
079
080   //-----------------------------------------------------------------------------------------------------------------
081   // Builder
082   //-----------------------------------------------------------------------------------------------------------------
083
084   /**
085    * Builder class.
086    */
087   @FluentSetters
088   public static class Builder extends UonSerializerSession.Builder {
089
090      OpenApiSerializer ctx;
091
092      /**
093       * Constructor
094       *
095       * @param ctx The context creating this session.
096       */
097      protected Builder(OpenApiSerializer ctx) {
098         super(ctx);
099         this.ctx = ctx;
100      }
101
102      @Override
103      public OpenApiSerializerSession build() {
104         return new OpenApiSerializerSession(this);
105      }
106
107      // <FluentSetters>
108
109      @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
110      public <T> Builder apply(Class<T> type, Consumer<T> apply) {
111         super.apply(type, apply);
112         return this;
113      }
114
115      @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
116      public Builder debug(Boolean value) {
117         super.debug(value);
118         return this;
119      }
120
121      @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
122      public Builder properties(Map<String,Object> value) {
123         super.properties(value);
124         return this;
125      }
126
127      @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
128      public Builder property(String key, Object value) {
129         super.property(key, value);
130         return this;
131      }
132
133      @Override /* GENERATED - org.apache.juneau.ContextSession.Builder */
134      public Builder unmodifiable() {
135         super.unmodifiable();
136         return this;
137      }
138
139      @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
140      public Builder locale(Locale value) {
141         super.locale(value);
142         return this;
143      }
144
145      @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
146      public Builder localeDefault(Locale value) {
147         super.localeDefault(value);
148         return this;
149      }
150
151      @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
152      public Builder mediaType(MediaType value) {
153         super.mediaType(value);
154         return this;
155      }
156
157      @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
158      public Builder mediaTypeDefault(MediaType value) {
159         super.mediaTypeDefault(value);
160         return this;
161      }
162
163      @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
164      public Builder timeZone(TimeZone value) {
165         super.timeZone(value);
166         return this;
167      }
168
169      @Override /* GENERATED - org.apache.juneau.BeanSession.Builder */
170      public Builder timeZoneDefault(TimeZone value) {
171         super.timeZoneDefault(value);
172         return this;
173      }
174
175      @Override /* GENERATED - org.apache.juneau.serializer.SerializerSession.Builder */
176      public Builder javaMethod(Method value) {
177         super.javaMethod(value);
178         return this;
179      }
180
181      @Override /* GENERATED - org.apache.juneau.serializer.SerializerSession.Builder */
182      public Builder resolver(VarResolverSession value) {
183         super.resolver(value);
184         return this;
185      }
186
187      @Override /* GENERATED - org.apache.juneau.serializer.SerializerSession.Builder */
188      public Builder schema(HttpPartSchema value) {
189         super.schema(value);
190         return this;
191      }
192
193      @Override /* GENERATED - org.apache.juneau.serializer.SerializerSession.Builder */
194      public Builder schemaDefault(HttpPartSchema value) {
195         super.schemaDefault(value);
196         return this;
197      }
198
199      @Override /* GENERATED - org.apache.juneau.serializer.SerializerSession.Builder */
200      public Builder uriContext(UriContext value) {
201         super.uriContext(value);
202         return this;
203      }
204
205      @Override /* GENERATED - org.apache.juneau.serializer.WriterSerializerSession.Builder */
206      public Builder fileCharset(Charset value) {
207         super.fileCharset(value);
208         return this;
209      }
210
211      @Override /* GENERATED - org.apache.juneau.serializer.WriterSerializerSession.Builder */
212      public Builder streamCharset(Charset value) {
213         super.streamCharset(value);
214         return this;
215      }
216
217      @Override /* GENERATED - org.apache.juneau.serializer.WriterSerializerSession.Builder */
218      public Builder useWhitespace(Boolean value) {
219         super.useWhitespace(value);
220         return this;
221      }
222
223      @Override /* GENERATED - org.apache.juneau.uon.UonSerializerSession.Builder */
224      public Builder encoding(boolean value) {
225         super.encoding(value);
226         return this;
227      }
228
229      // </FluentSetters>
230   }
231
232   //-----------------------------------------------------------------------------------------------------------------
233   // Instance
234   //-----------------------------------------------------------------------------------------------------------------
235
236   private final OpenApiSerializer ctx;
237
238   /**
239    * Constructor.
240    *
241    * @param builder The builder for this object.
242    */
243   protected OpenApiSerializerSession(Builder builder) {
244      super(builder.encoding(false));
245      ctx = builder.ctx;
246   }
247
248   @Override /* Serializer */
249   protected void doSerialize(SerializerPipe out, Object o) throws IOException, SerializeException {
250      try {
251         out.getWriter().write(serialize(HttpPartType.BODY, getSchema(), o));
252      } catch (SchemaValidationException e) {
253         throw new SerializeException(e);
254      }
255   }
256
257   @SuppressWarnings("rawtypes")
258   @Override /* PartSerializer */
259   public String serialize(HttpPartType partType, HttpPartSchema schema, Object value) throws SerializeException, SchemaValidationException {
260
261      ClassMeta<?> type = getClassMetaForObject(value);
262      if (type == null)
263         type = object();
264
265      // Swap if necessary
266      ObjectSwap swap = type.getSwap(this);
267      if (swap != null && ! type.isDateOrCalendarOrTemporal()) {
268         value = swap(swap, value);
269         type = swap.getSwapClassMeta(this);
270
271         // If the getSwapClass() method returns Object, we need to figure out
272         // the actual type now.
273         if (type.isObject())
274            type = getClassMetaForObject(value);
275      }
276
277      schema = ObjectUtils.firstNonNull(schema, DEFAULT_SCHEMA);
278
279      HttpPartDataType t = schema.getType(type);
280
281      HttpPartFormat f = schema.getFormat(type);
282      if (f == HttpPartFormat.NO_FORMAT)
283         f = ctx.getFormat();
284
285      HttpPartCollectionFormat cf = schema.getCollectionFormat();
286      if (cf == HttpPartCollectionFormat.NO_COLLECTION_FORMAT)
287         cf = ctx.getCollectionFormat();
288
289      String out = null;
290
291      schema.validateOutput(value, ctx.getBeanContext());
292
293      if (type.hasMutaterTo(schema.getParsedType()) || schema.getParsedType().hasMutaterFrom(type)) {
294         value = toType(value, schema.getParsedType());
295         type = schema.getParsedType();
296      }
297
298      if (type.isUri()) {
299         value = getUriResolver().resolve(value);
300         type = string();
301      }
302
303      if (value != null) {
304
305         if (t == STRING) {
306
307            if (f == BYTE) {
308               out = base64Encode(toType(value, CM_ByteArray));
309            } else if (f == BINARY) {
310               out = toHex(toType(value, CM_ByteArray));
311            } else if (f == BINARY_SPACED) {
312               out = toSpacedHex(toType(value, CM_ByteArray));
313            } else if (f == DATE) {
314               try {
315                  if (value instanceof Calendar)
316                     out = TemporalCalendarSwap.IsoDate.DEFAULT.swap(this, (Calendar)value);
317                  else if (value instanceof Date)
318                     out = TemporalDateSwap.IsoDate.DEFAULT.swap(this, (Date)value);
319                  else if (value instanceof Temporal)
320                     out = TemporalSwap.IsoDate.DEFAULT.swap(this, (Temporal)value);
321                  else
322                     out = value.toString();
323               } catch (Exception e) {
324                  throw new SerializeException(e);
325               }
326            } else if (f == DATE_TIME) {
327               try {
328                  if (value instanceof Calendar)
329                     out = TemporalCalendarSwap.IsoInstant.DEFAULT.swap(this, (Calendar)value);
330                  else if (value instanceof Date)
331                     out = TemporalDateSwap.IsoInstant.DEFAULT.swap(this, (Date)value);
332                  else if (value instanceof Temporal)
333                     out = TemporalSwap.IsoInstant.DEFAULT.swap(this, (Temporal)value);
334                  else
335                     out = value.toString();
336               } catch (Exception e) {
337                  throw new SerializeException(e);
338               }
339            } else if (f == HttpPartFormat.UON) {
340               out = super.serialize(partType, schema, value);
341            } else {
342               out = toType(value, string());
343            }
344
345         } else if (t == BOOLEAN) {
346
347            out = stringify(toType(value, CM_Boolean));
348
349         } else if (t == INTEGER) {
350
351            if (f == INT64)
352               out = stringify(toType(value, CM_Long));
353            else
354               out = stringify(toType(value, CM_Integer));
355
356         } else if (t == NUMBER) {
357
358            if (f == DOUBLE)
359               out = stringify(toType(value, CM_Double));
360            else
361               out = stringify(toType(value, CM_Float));
362
363         } else if (t == ARRAY) {
364
365            if (cf == HttpPartCollectionFormat.UONC)
366               out = super.serialize(partType, null, toList(partType, type, value, schema));
367            else {
368
369               HttpPartSchema items = schema.getItems();
370               ClassMeta<?> vt = getClassMetaForObject(value);
371               OapiStringBuilder sb = new OapiStringBuilder(cf);
372
373               if (type.isArray()) {
374                  for (int i = 0; i < Array.getLength(value); i++)
375                     sb.append(serialize(partType, items, Array.get(value, i)));
376               } else if (type.isCollection()) {
377                  ((Collection<?>)value).forEach(x -> sb.append(serialize(partType, items, x)));
378               } else if (vt.hasMutaterTo(String[].class)) {
379                  String[] ss = toType(value, CM_StringArray);
380                  for (int i = 0; i < ss.length; i++)
381                     sb.append(serialize(partType, items, ss[i]));
382               } else {
383                  throw new SerializeException("Input is not a valid array type: " + type);
384               }
385
386               out = sb.toString();
387            }
388
389         } else if (t == OBJECT) {
390
391            if (cf == HttpPartCollectionFormat.UONC) {
392               if (schema.hasProperties() && type.isMapOrBean())
393                  value = toMap(partType, type, value, schema);
394               out = super.serialize(partType, null, value);
395
396            } else if (type.isBean()) {
397               OapiStringBuilder sb = new OapiStringBuilder(cf);
398               Predicate<Object> checkNull = x -> isKeepNullProperties() || x != null;
399               HttpPartSchema schema2 = schema;
400
401               toBeanMap(value).forEachValue(checkNull, (pMeta,key,val,thrown) -> {
402                  if (thrown == null)
403                     sb.append(key, serialize(partType, schema2.getProperty(key), val));
404               });
405               out = sb.toString();
406
407            } else if (type.isMap()) {
408               OapiStringBuilder sb = new OapiStringBuilder(cf);
409               HttpPartSchema schema2 = schema;
410               ((Map<?,?>)value).forEach((k,v) -> sb.append(k, serialize(partType, schema2.getProperty(stringify(k)), v)));
411               out = sb.toString();
412
413            } else {
414               throw new SerializeException("Input is not a valid object type: " + type);
415            }
416
417         } else if (t == FILE) {
418            throw new SerializeException("File part not supported.");
419
420         } else if (t == NO_TYPE) {
421            // This should never be returned by HttpPartSchema.getType(ClassMeta).
422            throw new SerializeException("Invalid type.");
423         }
424      }
425
426      schema.validateInput(out);
427      if (out == null)
428         out = schema.getDefault();
429      if (out == null)
430         out = "null";
431      return out;
432   }
433
434   private static class OapiStringBuilder {
435      static final AsciiSet EQ = AsciiSet.create("=\\");
436      static final AsciiSet PIPE = AsciiSet.create("|\\");
437      static final AsciiSet PIPE_OR_EQ = AsciiSet.create("|=\\");
438      static final AsciiSet COMMA = AsciiSet.create(",\\");
439      static final AsciiSet COMMA_OR_EQ = AsciiSet.create(",=\\");
440
441      private final StringBuilder sb = new StringBuilder();
442      private final HttpPartCollectionFormat cf;
443      private boolean first = true;
444
445      OapiStringBuilder(HttpPartCollectionFormat cf) {
446         this.cf = cf;
447      }
448
449      private void delim(HttpPartCollectionFormat cf) {
450         if (cf == PIPES)
451            sb.append('|');
452         else if (cf == SSV)
453            sb.append(' ');
454         else if (cf == TSV)
455            sb.append('\t');
456         else
457            sb.append(',');
458      }
459
460      OapiStringBuilder append(Object o) {
461         if (! first)
462            delim(cf);
463         first = false;
464         if (cf == PIPES)
465            sb.append(escapeChars(stringify(o), PIPE));
466         else if (cf == SSV || cf == TSV)
467            sb.append(stringify(o));
468         else
469            sb.append(escapeChars(stringify(o), COMMA));
470         return this;
471      }
472
473      OapiStringBuilder append(Object key, Object val) {
474         if (! first)
475            delim(cf);
476         first = false;
477         if (cf == PIPES)
478            sb.append(escapeChars(stringify(key), PIPE_OR_EQ)).append('=').append(escapeChars(stringify(val), PIPE_OR_EQ));
479         else if (cf == SSV || cf == TSV)
480            sb.append(escapeChars(stringify(key), EQ)).append('=').append(escapeChars(stringify(val), EQ));
481         else
482            sb.append(escapeChars(stringify(key), COMMA_OR_EQ)).append('=').append(escapeChars(stringify(val), COMMA_OR_EQ));
483         return this;
484      }
485
486      @Override
487      public String toString() {
488         return sb.toString();
489      }
490   }
491
492   private Map<String,Object> toMap(HttpPartType partType, ClassMeta<?> type, Object o, HttpPartSchema s) throws SerializeException, SchemaValidationException {
493      if (s == null)
494         s = DEFAULT_SCHEMA;
495      JsonMap m = new JsonMap();
496      if (type.isBean()) {
497         Predicate<Object> checkNull = x -> isKeepNullProperties() || x != null;
498         HttpPartSchema s2 = s;
499         toBeanMap(o).forEachValue(checkNull, (pMeta,key,val,thrown) -> {
500            if (thrown == null)
501               m.put(key, toObject(partType, val, s2.getProperty(key)));
502         });
503      } else {
504         HttpPartSchema s2 = s;
505         ((Map<?,?>)o).forEach((k,v) -> m.put(stringify(k), toObject(partType, v, s2.getProperty(stringify(k)))));
506      }
507      if (isSortMaps())
508         return sort(m);
509      return m;
510   }
511
512   @SuppressWarnings("rawtypes")
513   private List toList(HttpPartType partType, ClassMeta<?> type, Object o, HttpPartSchema s) throws SerializeException, SchemaValidationException {
514      if (s == null)
515         s = DEFAULT_SCHEMA;
516      JsonList l = new JsonList();
517      HttpPartSchema items = s.getItems();
518      if (type.isArray()) {
519         for (int i = 0; i < Array.getLength(o); i++)
520            l.add(toObject(partType, Array.get(o, i), items));
521      } else if (type.isCollection()) {
522         ((Collection<?>)o).forEach(x -> l.add(toObject(partType, x, items)));
523      } else {
524         l.add(toObject(partType, o, items));
525      }
526      if (isSortCollections())
527         return sort(l);
528      return l;
529   }
530
531   @SuppressWarnings("rawtypes")
532   private Object toObject(HttpPartType partType, Object o, HttpPartSchema s) throws SerializeException, SchemaValidationException {
533      if (o == null)
534         return null;
535      if (s == null)
536         s = DEFAULT_SCHEMA;
537      ClassMeta cm = getClassMetaForObject(o);
538      HttpPartDataType t = s.getType(cm);
539      HttpPartFormat f = s.getFormat(cm);
540      HttpPartCollectionFormat cf = s.getCollectionFormat();
541
542      if (t == STRING) {
543         if (f == BYTE)
544            return base64Encode(toType(o, CM_ByteArray));
545         if (f == BINARY)
546            return toHex(toType(o, CM_ByteArray));
547         if (f == BINARY_SPACED)
548            return toSpacedHex(toType(o, CM_ByteArray));
549         if (f == DATE)
550            return toIsoDate(toType(o, CM_Calendar));
551         if (f == DATE_TIME)
552            return toIsoDateTime(toType(o, CM_Calendar));
553         return o;
554      } else if (t == ARRAY) {
555         List l = toList(partType, getClassMetaForObject(o), o, s);
556         if (cf == CSV)
557            return joine(l, ',');
558         if (cf == PIPES)
559            return joine(l, '|');
560         if (cf == SSV)
561            return join(l, ' ');
562         if (cf == TSV)
563            return join(l, '\t');
564         return l;
565      } else if (t == OBJECT) {
566         return toMap(partType, getClassMetaForObject(o), o, s);
567      }
568
569      return o;
570   }
571
572   private <T> T toType(Object in, ClassMeta<T> type) throws SerializeException {
573      try {
574         return convertToType(in, type);
575      } catch (InvalidDataConversionException e) {
576         throw new SerializeException(e);
577      }
578   }
579}