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.time.temporal.*;
028import java.util.*;
029import java.util.function.*;
030
031import org.apache.juneau.*;
032import org.apache.juneau.collections.*;
033import org.apache.juneau.common.utils.*;
034import org.apache.juneau.httppart.*;
035import org.apache.juneau.internal.*;
036import org.apache.juneau.serializer.*;
037import org.apache.juneau.svl.*;
038import org.apache.juneau.swap.*;
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 */
054public class OpenApiSerializerSession extends UonSerializerSession {
055
056   //-----------------------------------------------------------------------------------------------------------------
057   // Static
058   //-----------------------------------------------------------------------------------------------------------------
059
060   // Cache these for faster lookup
061   private static final BeanContext BC = BeanContext.DEFAULT;
062   private static final ClassMeta<byte[]> CM_ByteArray = BC.getClassMeta(byte[].class);
063   private static final ClassMeta<String[]> CM_StringArray = BC.getClassMeta(String[].class);
064   private static final ClassMeta<Calendar> CM_Calendar = BC.getClassMeta(Calendar.class);
065   private static final ClassMeta<Long> CM_Long = BC.getClassMeta(Long.class);
066   private static final ClassMeta<Integer> CM_Integer = BC.getClassMeta(Integer.class);
067   private static final ClassMeta<Double> CM_Double = BC.getClassMeta(Double.class);
068   private static final ClassMeta<Float> CM_Float = BC.getClassMeta(Float.class);
069   private static final ClassMeta<Boolean> CM_Boolean = BC.getClassMeta(Boolean.class);
070
071   private static final HttpPartSchema DEFAULT_SCHEMA = HttpPartSchema.DEFAULT;
072
073   /**
074    * Creates a new builder for this object.
075    *
076    * @param ctx The context creating this session.
077    * @return A new builder.
078    */
079   public static Builder create(OpenApiSerializer ctx) {
080      return new Builder(ctx);
081   }
082
083   //-----------------------------------------------------------------------------------------------------------------
084   // Builder
085   //-----------------------------------------------------------------------------------------------------------------
086
087   /**
088    * Builder class.
089    */
090   public static class Builder extends UonSerializerSession.Builder {
091
092      OpenApiSerializer ctx;
093
094      /**
095       * Constructor
096       *
097       * @param ctx The context creating this session.
098       */
099      protected Builder(OpenApiSerializer ctx) {
100         super(ctx);
101         this.ctx = ctx;
102      }
103
104      @Override
105      public OpenApiSerializerSession build() {
106         return new OpenApiSerializerSession(this);
107      }
108      @Override /* Overridden from Builder */
109      public <T> Builder apply(Class<T> type, Consumer<T> apply) {
110         super.apply(type, apply);
111         return this;
112      }
113
114      @Override /* Overridden from Builder */
115      public Builder debug(Boolean value) {
116         super.debug(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 unmodifiable() {
134         super.unmodifiable();
135         return this;
136      }
137
138      @Override /* Overridden from Builder */
139      public Builder locale(Locale value) {
140         super.locale(value);
141         return this;
142      }
143
144      @Override /* Overridden from Builder */
145      public Builder localeDefault(Locale value) {
146         super.localeDefault(value);
147         return this;
148      }
149
150      @Override /* Overridden from Builder */
151      public Builder mediaType(MediaType value) {
152         super.mediaType(value);
153         return this;
154      }
155
156      @Override /* Overridden from Builder */
157      public Builder mediaTypeDefault(MediaType value) {
158         super.mediaTypeDefault(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 javaMethod(Method value) {
176         super.javaMethod(value);
177         return this;
178      }
179
180      @Override /* Overridden from Builder */
181      public Builder resolver(VarResolverSession value) {
182         super.resolver(value);
183         return this;
184      }
185
186      @Override /* Overridden from Builder */
187      public Builder schema(HttpPartSchema value) {
188         super.schema(value);
189         return this;
190      }
191
192      @Override /* Overridden from Builder */
193      public Builder schemaDefault(HttpPartSchema value) {
194         super.schemaDefault(value);
195         return this;
196      }
197
198      @Override /* Overridden from Builder */
199      public Builder uriContext(UriContext value) {
200         super.uriContext(value);
201         return this;
202      }
203
204      @Override /* Overridden from Builder */
205      public Builder fileCharset(Charset value) {
206         super.fileCharset(value);
207         return this;
208      }
209
210      @Override /* Overridden from Builder */
211      public Builder streamCharset(Charset value) {
212         super.streamCharset(value);
213         return this;
214      }
215
216      @Override /* Overridden from Builder */
217      public Builder useWhitespace(Boolean value) {
218         super.useWhitespace(value);
219         return this;
220      }
221
222      @Override /* Overridden from Builder */
223      public Builder encoding(boolean value) {
224         super.encoding(value);
225         return this;
226      }
227   }
228
229   //-----------------------------------------------------------------------------------------------------------------
230   // Instance
231   //-----------------------------------------------------------------------------------------------------------------
232
233   private final OpenApiSerializer ctx;
234
235   /**
236    * Constructor.
237    *
238    * @param builder The builder for this object.
239    */
240   protected OpenApiSerializerSession(Builder builder) {
241      super(builder.encoding(false));
242      ctx = builder.ctx;
243   }
244
245   @Override /* Serializer */
246   protected void doSerialize(SerializerPipe out, Object o) throws IOException, SerializeException {
247      try {
248         out.getWriter().write(serialize(HttpPartType.BODY, getSchema(), o));
249      } catch (SchemaValidationException e) {
250         throw new SerializeException(e);
251      }
252   }
253
254   @SuppressWarnings("rawtypes")
255   @Override /* PartSerializer */
256   public String serialize(HttpPartType partType, HttpPartSchema schema, Object value) throws SerializeException, SchemaValidationException {
257
258      ClassMeta<?> type = getClassMetaForObject(value);
259      if (type == null)
260         type = object();
261
262      // Swap if necessary
263      ObjectSwap swap = type.getSwap(this);
264      if (swap != null && ! type.isDateOrCalendarOrTemporal()) {
265         value = swap(swap, value);
266         type = swap.getSwapClassMeta(this);
267
268         // If the getSwapClass() method returns Object, we need to figure out
269         // the actual type now.
270         if (type.isObject())
271            type = getClassMetaForObject(value);
272      }
273
274      schema = Utils.firstNonNull(schema, DEFAULT_SCHEMA);
275
276      HttpPartDataType t = schema.getType(type);
277
278      HttpPartFormat f = schema.getFormat(type);
279      if (f == HttpPartFormat.NO_FORMAT)
280         f = ctx.getFormat();
281
282      HttpPartCollectionFormat cf = schema.getCollectionFormat();
283      if (cf == HttpPartCollectionFormat.NO_COLLECTION_FORMAT)
284         cf = ctx.getCollectionFormat();
285
286      String out = null;
287
288      schema.validateOutput(value, ctx.getBeanContext());
289
290      if (type.hasMutaterTo(schema.getParsedType()) || schema.getParsedType().hasMutaterFrom(type)) {
291         value = toType(value, schema.getParsedType());
292         type = schema.getParsedType();
293      }
294
295      if (type.isUri()) {
296         value = getUriResolver().resolve(value);
297         type = string();
298      }
299
300      if (value != null) {
301
302         if (t == STRING) {
303
304            if (f == BYTE) {
305               out = base64Encode(toType(value, CM_ByteArray));
306            } else if (f == BINARY) {
307               out = toHex(toType(value, CM_ByteArray));
308            } else if (f == BINARY_SPACED) {
309               out = toSpacedHex(toType(value, CM_ByteArray));
310            } else if (f == DATE) {
311               try {
312                  if (value instanceof Calendar)
313                     out = TemporalCalendarSwap.IsoDate.DEFAULT.swap(this, (Calendar)value);
314                  else if (value instanceof Date)
315                     out = TemporalDateSwap.IsoDate.DEFAULT.swap(this, (Date)value);
316                  else if (value instanceof Temporal)
317                     out = TemporalSwap.IsoDate.DEFAULT.swap(this, (Temporal)value);
318                  else
319                     out = value.toString();
320               } catch (Exception e) {
321                  throw new SerializeException(e);
322               }
323            } else if (f == DATE_TIME) {
324               try {
325                  if (value instanceof Calendar)
326                     out = TemporalCalendarSwap.IsoInstant.DEFAULT.swap(this, (Calendar)value);
327                  else if (value instanceof Date)
328                     out = TemporalDateSwap.IsoInstant.DEFAULT.swap(this, (Date)value);
329                  else if (value instanceof Temporal)
330                     out = TemporalSwap.IsoInstant.DEFAULT.swap(this, (Temporal)value);
331                  else
332                     out = value.toString();
333               } catch (Exception e) {
334                  throw new SerializeException(e);
335               }
336            } else if (f == HttpPartFormat.UON) {
337               out = super.serialize(partType, schema, value);
338            } else {
339               out = toType(value, string());
340            }
341
342         } else if (t == BOOLEAN) {
343
344            out = Utils.s(toType(value, CM_Boolean));
345
346         } else if (t == INTEGER) {
347
348            if (f == INT64)
349               out = Utils.s(toType(value, CM_Long));
350            else
351               out = Utils.s(toType(value, CM_Integer));
352
353         } else if (t == NUMBER) {
354
355            if (f == DOUBLE)
356               out = Utils.s(toType(value, CM_Double));
357            else
358               out = Utils.s(toType(value, CM_Float));
359
360         } else if (t == ARRAY) {
361
362            if (cf == HttpPartCollectionFormat.UONC)
363               out = super.serialize(partType, null, toList(partType, type, value, schema));
364            else {
365
366               HttpPartSchema items = schema.getItems();
367               ClassMeta<?> vt = getClassMetaForObject(value);
368               OapiStringBuilder sb = new OapiStringBuilder(cf);
369
370               if (type.isArray()) {
371                  for (int i = 0; i < Array.getLength(value); i++)
372                     sb.append(serialize(partType, items, Array.get(value, i)));
373               } else if (type.isCollection()) {
374                  ((Collection<?>)value).forEach(x -> sb.append(serialize(partType, items, x)));
375               } else if (vt.hasMutaterTo(String[].class)) {
376                  String[] ss = toType(value, CM_StringArray);
377                  for (String element : ss)
378                            sb.append(serialize(partType, items, element));
379               } else {
380                  throw new SerializeException("Input is not a valid array type: " + type);
381               }
382
383               out = sb.toString();
384            }
385
386         } else if (t == OBJECT) {
387
388            if (cf == HttpPartCollectionFormat.UONC) {
389               if (schema.hasProperties() && type.isMapOrBean())
390                  value = toMap(partType, type, value, schema);
391               out = super.serialize(partType, null, value);
392
393            } else if (type.isBean()) {
394               OapiStringBuilder sb = new OapiStringBuilder(cf);
395               Predicate<Object> checkNull = x -> isKeepNullProperties() || x != null;
396               HttpPartSchema schema2 = schema;
397
398               toBeanMap(value).forEachValue(checkNull, (pMeta,key,val,thrown) -> {
399                  if (thrown == null)
400                     sb.append(key, serialize(partType, schema2.getProperty(key), val));
401               });
402               out = sb.toString();
403
404            } else if (type.isMap()) {
405               OapiStringBuilder sb = new OapiStringBuilder(cf);
406               HttpPartSchema schema2 = schema;
407               ((Map<?,?>)value).forEach((k,v) -> sb.append(k, serialize(partType, schema2.getProperty(Utils.s(k)), v)));
408               out = sb.toString();
409
410            } else {
411               throw new SerializeException("Input is not a valid object type: " + type);
412            }
413
414         } else if (t == FILE) {
415            throw new SerializeException("File part not supported.");
416
417         } else if (t == NO_TYPE) {
418            // This should never be returned by HttpPartSchema.getType(ClassMeta).
419            throw new SerializeException("Invalid type.");
420         }
421      }
422
423      schema.validateInput(out);
424      if (out == null)
425         out = schema.getDefault();
426      if (out == null)
427         out = "null";
428      return out;
429   }
430
431   private static class OapiStringBuilder {
432      static final AsciiSet EQ = AsciiSet.of("=\\");
433      static final AsciiSet PIPE = AsciiSet.of("|\\");
434      static final AsciiSet PIPE_OR_EQ = AsciiSet.of("|=\\");
435      static final AsciiSet COMMA = AsciiSet.of(",\\");
436      static final AsciiSet COMMA_OR_EQ = AsciiSet.of(",=\\");
437
438      private final StringBuilder sb = new StringBuilder();
439      private final HttpPartCollectionFormat cf;
440      private boolean first = true;
441
442      OapiStringBuilder(HttpPartCollectionFormat cf) {
443         this.cf = cf;
444      }
445
446      private void delim(HttpPartCollectionFormat cf) {
447         if (cf == PIPES)
448            sb.append('|');
449         else if (cf == SSV)
450            sb.append(' ');
451         else if (cf == TSV)
452            sb.append('\t');
453         else
454            sb.append(',');
455      }
456
457      OapiStringBuilder append(Object o) {
458         if (! first)
459            delim(cf);
460         first = false;
461         if (cf == PIPES)
462            sb.append(escapeChars(Utils.s(o), PIPE));
463         else if (cf == SSV || cf == TSV)
464            sb.append(Utils.s(o));
465         else
466            sb.append(escapeChars(Utils.s(o), COMMA));
467         return this;
468      }
469
470      OapiStringBuilder append(Object key, Object val) {
471         if (! first)
472            delim(cf);
473         first = false;
474         if (cf == PIPES)
475            sb.append(escapeChars(Utils.s(key), PIPE_OR_EQ)).append('=').append(escapeChars(Utils.s(val), PIPE_OR_EQ));
476         else if (cf == SSV || cf == TSV)
477            sb.append(escapeChars(Utils.s(key), EQ)).append('=').append(escapeChars(Utils.s(val), EQ));
478         else
479            sb.append(escapeChars(Utils.s(key), COMMA_OR_EQ)).append('=').append(escapeChars(Utils.s(val), COMMA_OR_EQ));
480         return this;
481      }
482
483      @Override
484      public String toString() {
485         return sb.toString();
486      }
487   }
488
489   private Map<String,Object> toMap(HttpPartType partType, ClassMeta<?> type, Object o, HttpPartSchema s) throws SerializeException, SchemaValidationException {
490      if (s == null)
491         s = DEFAULT_SCHEMA;
492      JsonMap m = new JsonMap();
493      if (type.isBean()) {
494         Predicate<Object> checkNull = x -> isKeepNullProperties() || x != null;
495         HttpPartSchema s2 = s;
496         toBeanMap(o).forEachValue(checkNull, (pMeta,key,val,thrown) -> {
497            if (thrown == null)
498               m.put(key, toObject(partType, val, s2.getProperty(key)));
499         });
500      } else {
501         HttpPartSchema s2 = s;
502         ((Map<?,?>)o).forEach((k,v) -> m.put(Utils.s(k), toObject(partType, v, s2.getProperty(Utils.s(k)))));
503      }
504      if (isSortMaps())
505         return sort(m);
506      return m;
507   }
508
509   @SuppressWarnings("rawtypes")
510   private List toList(HttpPartType partType, ClassMeta<?> type, Object o, HttpPartSchema s) throws SerializeException, SchemaValidationException {
511      if (s == null)
512         s = DEFAULT_SCHEMA;
513      JsonList l = new JsonList();
514      HttpPartSchema items = s.getItems();
515      if (type.isArray()) {
516         for (int i = 0; i < Array.getLength(o); i++)
517            l.add(toObject(partType, Array.get(o, i), items));
518      } else if (type.isCollection()) {
519         ((Collection<?>)o).forEach(x -> l.add(toObject(partType, x, items)));
520      } else {
521         l.add(toObject(partType, o, items));
522      }
523      if (isSortCollections())
524         return sort(l);
525      return l;
526   }
527
528   @SuppressWarnings("rawtypes")
529   private Object toObject(HttpPartType partType, Object o, HttpPartSchema s) throws SerializeException, SchemaValidationException {
530      if (o == null)
531         return null;
532      if (s == null)
533         s = DEFAULT_SCHEMA;
534      ClassMeta cm = getClassMetaForObject(o);
535      HttpPartDataType t = s.getType(cm);
536      HttpPartFormat f = s.getFormat(cm);
537      HttpPartCollectionFormat cf = s.getCollectionFormat();
538
539      if (t == STRING) {
540         if (f == BYTE)
541            return base64Encode(toType(o, CM_ByteArray));
542         if (f == BINARY)
543            return toHex(toType(o, CM_ByteArray));
544         if (f == BINARY_SPACED)
545            return toSpacedHex(toType(o, CM_ByteArray));
546         if (f == DATE)
547            return toIsoDate(toType(o, CM_Calendar));
548         if (f == DATE_TIME)
549            return toIsoDateTime(toType(o, CM_Calendar));
550         return o;
551      } else if (t == ARRAY) {
552         List l = toList(partType, getClassMetaForObject(o), o, s);
553         if (cf == CSV)
554            return StringUtils.joine(l, ',');
555         if (cf == PIPES)
556            return StringUtils.joine(l, '|');
557         if (cf == SSV)
558            return Utils.join(l, ' ');
559         if (cf == TSV)
560            return Utils.join(l, '\t');
561         return l;
562      } else if (t == OBJECT) {
563         return toMap(partType, getClassMetaForObject(o), o, s);
564      }
565
566      return o;
567   }
568
569   private <T> T toType(Object in, ClassMeta<T> type) throws SerializeException {
570      try {
571         return convertToType(in, type);
572      } catch (InvalidDataConversionException e) {
573         throw new SerializeException(e);
574      }
575   }
576}