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.httppart.HttpPartCollectionFormat.*;
016import static org.apache.juneau.httppart.HttpPartDataType.*;
017import static org.apache.juneau.httppart.HttpPartFormat.*;
018import static org.apache.juneau.internal.StringUtils.*;
019
020import java.io.IOException;
021import java.lang.reflect.*;
022import java.time.temporal.*;
023import java.util.*;
024
025import org.apache.juneau.*;
026import org.apache.juneau.collections.*;
027import org.apache.juneau.httppart.*;
028import org.apache.juneau.internal.*;
029import org.apache.juneau.serializer.*;
030import org.apache.juneau.transform.*;
031import org.apache.juneau.transforms.*;
032import org.apache.juneau.uon.*;
033
034/**
035 * Session object that lives for the duration of a single use of {@link OpenApiSerializer}.
036 *
037 * <p>
038 * This class is NOT thread safe.
039 * It is typically discarded after one-time use although it can be reused within the same thread.
040 */
041public class OpenApiSerializerSession extends UonSerializerSession {
042
043   //-------------------------------------------------------------------------------------------------------------------
044   // Predefined instances
045   //-------------------------------------------------------------------------------------------------------------------
046
047   // Cache these for faster lookup
048   private static final BeanContext BC = BeanContext.DEFAULT;
049   private static final ClassMeta<byte[]> CM_ByteArray = BC.getClassMeta(byte[].class);
050   private static final ClassMeta<String[]> CM_StringArray = BC.getClassMeta(String[].class);
051   private static final ClassMeta<Calendar> CM_Calendar = BC.getClassMeta(Calendar.class);
052   private static final ClassMeta<Long> CM_Long = BC.getClassMeta(Long.class);
053   private static final ClassMeta<Integer> CM_Integer = BC.getClassMeta(Integer.class);
054   private static final ClassMeta<Double> CM_Double = BC.getClassMeta(Double.class);
055   private static final ClassMeta<Float> CM_Float = BC.getClassMeta(Float.class);
056   private static final ClassMeta<Boolean> CM_Boolean = BC.getClassMeta(Boolean.class);
057
058   private static final HttpPartSchema DEFAULT_SCHEMA = HttpPartSchema.DEFAULT;
059
060   //-------------------------------------------------------------------------------------------------------------------
061   // Instance
062   //-------------------------------------------------------------------------------------------------------------------
063
064   private final OpenApiSerializer ctx;
065
066   /**
067    * Create a new session using properties specified in the context.
068    *
069    * @param ctx
070    *    The context creating this session object.
071    *    The context contains all the configuration settings for this object.
072    * @param args
073    *    Runtime session arguments.
074    */
075   protected OpenApiSerializerSession(OpenApiSerializer ctx, SerializerSessionArgs args) {
076      super(ctx, false, args);
077      this.ctx = ctx;
078   }
079
080   //-----------------------------------------------------------------------------------------------------------------
081   // Entry point methods
082   //-----------------------------------------------------------------------------------------------------------------
083
084   @Override /* Serializer */
085   protected void doSerialize(SerializerPipe out, Object o) throws IOException, SerializeException {
086      try {
087         out.getWriter().write(serialize(HttpPartType.BODY, getSchema(), o));
088      } catch (SchemaValidationException e) {
089         throw new SerializeException(e);
090      }
091   }
092
093   @SuppressWarnings("rawtypes")
094   @Override /* PartSerializer */
095   public String serialize(HttpPartType partType, HttpPartSchema schema, Object value) throws SerializeException, SchemaValidationException {
096
097      ClassMeta<?> type = getClassMetaForObject(value);
098      if (type == null)
099         type = object();
100
101      // Swap if necessary
102      PojoSwap swap = type.getSwap(this);
103      if (swap != null && ! type.isDateOrCalendarOrTemporal()) {
104         value = swap(swap, value);
105         type = swap.getSwapClassMeta(this);
106
107         // If the getSwapClass() method returns Object, we need to figure out
108         // the actual type now.
109         if (type.isObject())
110            type = getClassMetaForObject(value);
111      }
112
113      schema = ObjectUtils.firstNonNull(schema, DEFAULT_SCHEMA);
114
115      HttpPartDataType t = schema.getType(type);
116
117      HttpPartFormat f = schema.getFormat(type);
118      if (f == HttpPartFormat.NO_FORMAT)
119         f = ctx.getFormat();
120
121      HttpPartCollectionFormat cf = schema.getCollectionFormat();
122      if (cf == HttpPartCollectionFormat.NO_COLLECTION_FORMAT)
123         cf = ctx.getCollectionFormat();
124
125      String out = null;
126
127      schema.validateOutput(value, ctx);
128
129      if (type.hasMutaterTo(schema.getParsedType()) || schema.getParsedType().hasMutaterFrom(type)) {
130         value = toType(value, schema.getParsedType());
131         type = schema.getParsedType();
132      }
133
134      if (type.isUri()) {
135         value = getUriResolver().resolve(value);
136         type = string();
137      }
138
139      if (value != null) {
140
141         if (t == STRING) {
142
143            if (f == BYTE) {
144               out = base64Encode(toType(value, CM_ByteArray));
145            } else if (f == BINARY) {
146               out = toHex(toType(value, CM_ByteArray));
147            } else if (f == BINARY_SPACED) {
148               out = toSpacedHex(toType(value, CM_ByteArray));
149            } else if (f == DATE) {
150               try {
151                  if (value instanceof Calendar)
152                     out = TemporalCalendarSwap.IsoDate.DEFAULT.swap(this, (Calendar)value);
153                  else if (value instanceof Date)
154                     out = TemporalDateSwap.IsoDate.DEFAULT.swap(this, (Date)value);
155                  else if (value instanceof Temporal)
156                     out = TemporalSwap.IsoDate.DEFAULT.swap(this, (Temporal)value);
157                  else
158                     out = value.toString();
159               } catch (Exception e) {
160                  throw new SerializeException(e);
161               }
162            } else if (f == DATE_TIME) {
163               try {
164                  if (value instanceof Calendar)
165                     out = TemporalCalendarSwap.IsoInstant.DEFAULT.swap(this, (Calendar)value);
166                  else if (value instanceof Date)
167                     out = TemporalDateSwap.IsoInstant.DEFAULT.swap(this, (Date)value);
168                  else if (value instanceof Temporal)
169                     out = TemporalSwap.IsoInstant.DEFAULT.swap(this, (Temporal)value);
170                  else
171                     out = value.toString();
172               } catch (Exception e) {
173                  throw new SerializeException(e);
174               }
175            } else if (f == HttpPartFormat.UON) {
176               out = super.serialize(partType, schema, value);
177            } else {
178               out = toType(value, string());
179            }
180
181         } else if (t == BOOLEAN) {
182
183            out = stringify(toType(value, CM_Boolean));
184
185         } else if (t == INTEGER) {
186
187            if (f == INT64)
188               out = stringify(toType(value, CM_Long));
189            else
190               out = stringify(toType(value, CM_Integer));
191
192         } else if (t == NUMBER) {
193
194            if (f == DOUBLE)
195               out = stringify(toType(value, CM_Double));
196            else
197               out = stringify(toType(value, CM_Float));
198
199         } else if (t == ARRAY) {
200
201            if (cf == HttpPartCollectionFormat.UONC)
202               out = super.serialize(partType, null, toList(partType, type, value, schema));
203            else {
204
205               HttpPartSchema items = schema.getItems();
206               ClassMeta<?> vt = getClassMetaForObject(value);
207               OapiStringBuilder sb = new OapiStringBuilder(cf);
208
209               if (type.isArray()) {
210                  for (int i = 0; i < Array.getLength(value); i++)
211                     sb.append(serialize(partType, items, Array.get(value, i)));
212               } else if (type.isCollection()) {
213                  for (Object o : (Collection<?>)value)
214                     sb.append(serialize(partType, items, o));
215               } else if (vt.hasMutaterTo(String[].class)) {
216                  String[] ss = toType(value, CM_StringArray);
217                  for (int i = 0; i < ss.length; i++)
218                     sb.append(serialize(partType, items, ss[i]));
219               } else {
220                  throw new SerializeException("Input is not a valid array type: " + type);
221               }
222
223               out = sb.toString();
224            }
225
226         } else if (t == OBJECT) {
227
228            if (cf == HttpPartCollectionFormat.UONC) {
229               if (schema.hasProperties() && type.isMapOrBean())
230                  value = toMap(partType, type, value, schema);
231               out = super.serialize(partType, null, value);
232
233            } else if (type.isBean()) {
234               OapiStringBuilder sb = new OapiStringBuilder(cf);
235               for (BeanPropertyValue p : toBeanMap(value).getValues(isKeepNullProperties())) {
236                  if (p.getMeta().canRead()) {
237                     Throwable x = p.getThrown();
238                     if (x == null)
239                        sb.append(p.getName(), serialize(partType, schema.getProperty(p.getName()), p.getValue()));
240                  }
241               }
242               out = sb.toString();
243
244            } else if (type.isMap()) {
245               OapiStringBuilder sb = new OapiStringBuilder(cf);
246               for (Map.Entry e : (Set<Map.Entry>)((Map)value).entrySet())
247                  sb.append(e.getKey(), serialize(partType, schema.getProperty(stringify(e.getKey())), e.getValue()));
248               out = sb.toString();
249
250            } else {
251               throw new SerializeException("Input is not a valid object type: " + type);
252            }
253
254         } else if (t == FILE) {
255            throw new SerializeException("File part not supported.");
256
257         } else if (t == NO_TYPE) {
258            // This should never be returned by HttpPartSchema.getType(ClassMeta).
259            throw new SerializeException("Invalid type.");
260         }
261      }
262
263      schema.validateInput(out);
264      if (out == null)
265         out = schema.getDefault();
266      if (out == null)
267         out = "null";
268      return out;
269   }
270
271   private static class OapiStringBuilder {
272      static final AsciiSet EQ = AsciiSet.create("=\\");
273      static final AsciiSet PIPE = AsciiSet.create("|\\");
274      static final AsciiSet PIPE_OR_EQ = AsciiSet.create("|=\\");
275      static final AsciiSet COMMA = AsciiSet.create(",\\");
276      static final AsciiSet COMMA_OR_EQ = AsciiSet.create(",=\\");
277
278      private final StringBuilder sb = new StringBuilder();
279      private final HttpPartCollectionFormat cf;
280      private boolean first = true;
281
282      OapiStringBuilder(HttpPartCollectionFormat cf) {
283         this.cf = cf;
284      }
285
286      private void delim(HttpPartCollectionFormat cf) {
287         if (cf == PIPES)
288            sb.append('|');
289         else if (cf == SSV)
290            sb.append(' ');
291         else if (cf == TSV)
292            sb.append('\t');
293         else
294            sb.append(',');
295      }
296
297      OapiStringBuilder append(Object o) {
298         if (! first)
299            delim(cf);
300         first = false;
301         if (cf == PIPES)
302            sb.append(escapeChars(stringify(o), PIPE));
303         else if (cf == SSV || cf == TSV)
304            sb.append(stringify(o));
305         else
306            sb.append(escapeChars(stringify(o), COMMA));
307         return this;
308      }
309
310      OapiStringBuilder append(Object key, Object val) {
311         if (! first)
312            delim(cf);
313         first = false;
314         if (cf == PIPES)
315            sb.append(escapeChars(stringify(key), PIPE_OR_EQ)).append('=').append(escapeChars(stringify(val), PIPE_OR_EQ));
316         else if (cf == SSV || cf == TSV)
317            sb.append(escapeChars(stringify(key), EQ)).append('=').append(escapeChars(stringify(val), EQ));
318         else
319            sb.append(escapeChars(stringify(key), COMMA_OR_EQ)).append('=').append(escapeChars(stringify(val), COMMA_OR_EQ));
320         return this;
321      }
322
323      @Override
324      public String toString() {
325         return sb.toString();
326      }
327   }
328
329   @SuppressWarnings({ "rawtypes" })
330   private Map<String,Object> toMap(HttpPartType partType, ClassMeta<?> type, Object o, HttpPartSchema s) throws SerializeException, SchemaValidationException {
331      if (s == null)
332         s = DEFAULT_SCHEMA;
333      OMap m = new OMap();
334      if (type.isBean()) {
335         for (BeanPropertyValue p : toBeanMap(o).getValues(isKeepNullProperties())) {
336            if (p.getMeta().canRead()) {
337               Throwable t = p.getThrown();
338               if (t == null)
339                  m.put(p.getName(), toObject(partType, p.getValue(), s.getProperty(p.getName())));
340            }
341         }
342      } else {
343         for (Map.Entry e : (Set<Map.Entry>)((Map)o).entrySet())
344            m.put(stringify(e.getKey()), toObject(partType, e.getValue(), s.getProperty(stringify(e.getKey()))));
345      }
346      if (isSortMaps())
347         return sort(m);
348      return m;
349   }
350
351   @SuppressWarnings("rawtypes")
352   private Collection toList(HttpPartType partType, ClassMeta<?> type, Object o, HttpPartSchema s) throws SerializeException, SchemaValidationException {
353      if (s == null)
354         s = DEFAULT_SCHEMA;
355      OList l = new OList();
356      HttpPartSchema items = s.getItems();
357      if (type.isArray()) {
358         for (int i = 0; i < Array.getLength(o); i++)
359            l.add(toObject(partType, Array.get(o, i), items));
360      } else if (type.isCollection()) {
361         for (Object o2 : (Collection<?>)o)
362            l.add(toObject(partType, o2, items));
363      } else {
364         l.add(toObject(partType, o, items));
365      }
366      if (isSortCollections())
367         return sort(l);
368      return l;
369   }
370
371   @SuppressWarnings("rawtypes")
372   private Object toObject(HttpPartType partType, Object o, HttpPartSchema s) throws SerializeException, SchemaValidationException {
373      if (o == null)
374         return null;
375      if (s == null)
376         s = DEFAULT_SCHEMA;
377      ClassMeta cm = getClassMetaForObject(o);
378      HttpPartDataType t = s.getType(cm);
379      HttpPartFormat f = s.getFormat(cm);
380      HttpPartCollectionFormat cf = s.getCollectionFormat();
381
382      if (t == STRING) {
383         if (f == BYTE)
384            return base64Encode(toType(o, CM_ByteArray));
385         if (f == BINARY)
386            return toHex(toType(o, CM_ByteArray));
387         if (f == BINARY_SPACED)
388            return toSpacedHex(toType(o, CM_ByteArray));
389         if (f == DATE)
390            return toIsoDate(toType(o, CM_Calendar));
391         if (f == DATE_TIME)
392            return toIsoDateTime(toType(o, CM_Calendar));
393         return o;
394      } else if (t == ARRAY) {
395         Collection l = toList(partType, getClassMetaForObject(o), o, s);
396         if (cf == CSV)
397            return joine(l, ',');
398         if (cf == PIPES)
399            return joine(l, '|');
400         if (cf == SSV)
401            return join(l, ' ');
402         if (cf == TSV)
403            return join(l, '\t');
404         return l;
405      } else if (t == OBJECT) {
406         return toMap(partType, getClassMetaForObject(o), o, s);
407      }
408
409      return o;
410   }
411
412   private <T> T toType(Object in, ClassMeta<T> type) throws SerializeException {
413      try {
414         return convertToType(in, type);
415      } catch (InvalidDataConversionException e) {
416         throw new SerializeException(e);
417      }
418   }
419
420   //-----------------------------------------------------------------------------------------------------------------
421   // Other methods
422   //-----------------------------------------------------------------------------------------------------------------
423
424   @Override /* Session */
425   public OMap toMap() {
426      return super.toMap()
427         .a("OpenApiSerializerSession", new DefaultFilteringOMap()
428      );
429   }
430}