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.*;
029import java.util.function.*;
030
031import org.apache.juneau.commons.httppart.*;
032import org.apache.juneau.commons.lang.*;
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.serializer.*;
038import org.apache.juneau.marshall.uon.*;
039import org.apache.juneau.marshall.utils.*;
040
041/**
042 * Session object that lives for the duration of a single use of {@link OpenApiSerializer}.
043 *
044 * <h5 class='section'>Notes:</h5><ul>
045 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
046 * </ul>
047 *
048 * <h5 class='section'>See Also:</h5><ul>
049 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/OpenApiSupport">OpenApi</a>
050
051 * </ul>
052 */
053@SuppressWarnings({
054   "java:S110", // Inheritance depth acceptable for this class hierarchy
055   "java:S3776", // Cognitive complexity acceptable for this specific logic
056   "rawtypes", // toList returns a raw List and getClassMetaForObject results are held as raw ClassMeta
057   "resource" // Resource management handled externally
058})
059public class OpenApiSerializerSession extends UonSerializerSession {
060
061   /**
062    * Builder class.
063    */
064   public static class Builder extends UonSerializerSession.Builder<Builder> {
065
066      private final OpenApiSerializer ctx;
067
068      /**
069       * Constructor
070       *
071       * @param ctx The context creating this session.
072       */
073      protected Builder(OpenApiSerializer ctx) {
074         super(ctx);
075         this.ctx = ctx;
076      }
077
078      @Override
079      public OpenApiSerializerSession build() {
080         return new OpenApiSerializerSession(this);
081      }
082
083   }
084
085   private static class OapiStringBuilder {
086      static final AsciiSet EQ = AsciiSet.of("=\\");
087      static final AsciiSet PIPE = AsciiSet.of("|\\");
088      static final AsciiSet PIPE_OR_EQ = AsciiSet.of("|=\\");
089      static final AsciiSet COMMA = AsciiSet.of(",\\");
090      static final AsciiSet COMMA_OR_EQ = AsciiSet.of(",=\\");
091
092      private final StringBuilder sb = new StringBuilder();
093      private final HttpPartCollectionFormat cf;
094      private boolean first = true;
095
096      OapiStringBuilder(HttpPartCollectionFormat cf) {
097         this.cf = cf;
098      }
099
100      @Override
101      public String toString() {
102         return sb.toString();
103      }
104
105      private void delim(HttpPartCollectionFormat cf) {
106         if (cf == PIPES)
107            sb.append('|');
108         else if (cf == SSV)
109            sb.append(' ');
110         else if (cf == TSV)
111            sb.append('\t');
112         else
113            sb.append(',');
114      }
115
116      OapiStringBuilder append(Object o) {
117         if (! first)
118            delim(cf);
119         first = false;
120         if (cf == PIPES)
121            sb.append(escapeChars(s(o), PIPE));
122         else if (cf == SSV || cf == TSV)
123            sb.append(s(o));
124         else
125            sb.append(escapeChars(s(o), COMMA));
126         return this;
127      }
128
129      OapiStringBuilder append(Object key, Object val) {
130         if (! first)
131            delim(cf);
132         first = false;
133         if (cf == PIPES)
134            sb.append(escapeChars(s(key), PIPE_OR_EQ)).append('=').append(escapeChars(s(val), PIPE_OR_EQ));
135         else if (cf == SSV || cf == TSV)
136            sb.append(escapeChars(s(key), EQ)).append('=').append(escapeChars(s(val), EQ));
137         else
138            sb.append(escapeChars(s(key), COMMA_OR_EQ)).append('=').append(escapeChars(s(val), COMMA_OR_EQ));
139         return this;
140      }
141   }
142
143   // Cache these for faster lookup
144   private static final MarshallingContext BC = MarshallingContext.DEFAULT;
145   private static final ClassMeta<byte[]> CM_ByteArray = BC.getClassMeta(byte[].class);
146   private static final ClassMeta<String[]> CM_StringArray = BC.getClassMeta(String[].class);
147   private static final ClassMeta<Calendar> CM_Calendar = BC.getClassMeta(Calendar.class);
148   private static final ClassMeta<Long> CM_Long = BC.getClassMeta(Long.class);
149   private static final ClassMeta<Integer> CM_Integer = BC.getClassMeta(Integer.class);
150   private static final ClassMeta<Double> CM_Double = BC.getClassMeta(Double.class);
151
152   private static final ClassMeta<Float> CM_Float = BC.getClassMeta(Float.class);
153
154   private static final ClassMeta<Boolean> CM_Boolean = BC.getClassMeta(Boolean.class);
155   private static final HttpPartSchema DEFAULT_SCHEMA = HttpPartSchema.DEFAULT;
156
157   /**
158    * Creates a new builder for this object.
159    *
160    * @param ctx The context creating this session.
161    * @return A new builder.
162    */
163   public static Builder create(OpenApiSerializer ctx) {
164      return new Builder(ctx);
165   }
166
167   private final OpenApiSerializer ctx;
168
169   /**
170    * Constructor.
171    *
172    * @param builder The builder for this object.
173    */
174   protected OpenApiSerializerSession(Builder builder) {
175      super(builder);
176      ctx = builder.ctx;
177   }
178
179   @Override /* Overridden from PartSerializer */
180   @SuppressWarnings({
181      "java:S6541" // Single-threaded session contexts do not require synchronization
182   })
183   public String write(HttpPartType partType, HttpPartSchema schema, Object value) throws SerializeException, SchemaValidationException {
184
185      ClassMeta<?> type = getClassMetaForObject(value);
186      if (type == null)
187         type = object();
188
189      // Swap if necessary
190      var swap = type.getSwap(this);
191      if (nn(swap) && ! type.isDateOrCalendarOrTemporal()) {
192         value = swap(swap, value);
193         type = swap.getSwapClassMeta(this);
194
195         // If the getSwapClass() method returns Object, we need to figure out
196         // the actual type now.
197         if (type.isObject())
198            type = getClassMetaForObject(value);
199      }
200
201      schema = coalesce(schema, DEFAULT_SCHEMA);
202
203      HttpPartDataType t = schema.getType(type);
204
205      HttpPartFormat f = schema.getFormat(type);
206      if (f == HttpPartFormat.NO_FORMAT)
207         f = ctx.getFormat();
208
209      HttpPartCollectionFormat cf = schema.getCollectionFormat();
210      if (cf == HttpPartCollectionFormat.NO_COLLECTION_FORMAT)
211         cf = ctx.getCollectionFormat();
212
213      String out = null;
214
215      schema.validateOutput(value);
216
217      var spt = getClassMeta(schema.getParsedType());
218      if (type.hasMutaterTo(spt) || spt.hasMutaterFrom(type)) {
219         value = toType(value, spt);
220         type = spt;
221      }
222
223      if (type.isUri()) {
224         value = getUriResolver().resolve(value);
225         type = string();
226      }
227
228      if (nn(value)) {
229
230         if (t == STRING) {
231
232            if (f == BYTE) {
233               out = base64Encode(toType(value, CM_ByteArray));
234            } else if (f == BINARY) {
235               out = toHex(toType(value, CM_ByteArray));
236            } else if (f == BINARY_SPACED) {
237               out = toSpacedHex(toType(value, CM_ByteArray));
238            } else if (f == DATE) {
239               out = Iso8601Utils.formatAsDate(value, type, getTimeZone());
240            } else if (f == DATE_TIME) {
241               out = Iso8601Utils.formatAsDateTime(value, type, getTimeZone());
242            } else if (f == HttpPartFormat.UON) {
243               out = super.write(partType, schema, value);
244            } else {
245               out = toType(value, string());
246            }
247
248         } else if (t == BOOLEAN) {
249
250            out = s(toType(value, CM_Boolean));
251
252         } else if (t == INTEGER) {
253
254            if (f == INT64)
255               out = s(toType(value, CM_Long));
256            else
257               out = s(toType(value, CM_Integer));
258
259         } else if (t == NUMBER) {
260
261            if (f == DOUBLE)
262               out = s(toType(value, CM_Double));
263            else
264               out = s(toType(value, CM_Float));
265
266         } else if (t == ARRAY) {
267
268            if (cf == HttpPartCollectionFormat.UONC)
269               out = super.write(partType, null, toList(partType, type, value, schema));
270            else {
271
272               HttpPartSchema items = schema.getItems();
273               ClassMeta<?> vt = getClassMetaForObject(value);
274               var sb = new OapiStringBuilder(cf);
275
276               if (type.isArray()) {
277                  for (var i = 0; i < Array.getLength(value); i++)
278                     sb.append(write(partType, items, Array.get(value, i)));
279               } else if (type.isCollection()) {
280                  ((Collection<?>)value).forEach(x -> sb.append(write(partType, items, x)));
281               } else if (type.isStreamable()) {
282                  forEachStreamableEntry(value, type, x -> sb.append(write(partType, items, x)));
283               } else if (vt.hasMutaterTo(String[].class)) {
284                  String[] ss = toType(value, CM_StringArray);
285                  for (var element : ss)
286                     sb.append(write(partType, items, element));
287               } else {
288                  throw new SerializeException("Input is not a valid array type: " + type);
289               }
290
291               out = sb.toString();
292            }
293
294         } else if (t == OBJECT) {
295
296            if (cf == HttpPartCollectionFormat.UONC) {
297               if (schema.hasProperties() && type.isMapOrBean())
298                  value = toMap(partType, type, value, schema);
299               out = super.write(partType, null, value);
300
301            } else if (type.isBean()) {
302               var sb = new OapiStringBuilder(cf);
303               Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x);
304               HttpPartSchema schema2 = schema;
305
306               toBeanMap(value).forEachValue(checkNull, (pMeta, key, val, thrown) -> {
307                  if (thrown == null)
308                     sb.append(key, write(partType, schema2.getProperty(key), val));
309               });
310               out = sb.toString();
311
312            } else if (type.isMap()) {
313               var sb = new OapiStringBuilder(cf);
314               HttpPartSchema schema2 = schema;
315               ((Map<?,?>)value).forEach((k, v) -> sb.append(k, write(partType, schema2.getProperty(s(k)), v)));
316               out = sb.toString();
317
318            } else {
319               throw new SerializeException("Input is not a valid object type: " + type);
320            }
321
322         } else if (t == FILE) {
323            throw new SerializeException("File part not supported.");
324
325         } else if (t == NO_TYPE) {
326            // This should never be returned by HttpPartSchema.getType(ClassMeta).
327            throw new SerializeException("Invalid type.");
328         }
329      }
330
331      schema.validateInput(out);
332      if (out == null)
333         out = schema.getDefault();
334      if (out == null)
335         out = "null";
336      return out;
337   }
338
339   private List toList(HttpPartType partType, ClassMeta<?> type, Object o, HttpPartSchema s) throws SerializeException, SchemaValidationException {
340      if (s == null)
341         s = DEFAULT_SCHEMA;
342      var l = new JsonList();
343      HttpPartSchema items = s.getItems();
344      if (type.isArray()) {
345         for (var i = 0; i < Array.getLength(o); i++)
346            l.add(toObject(partType, Array.get(o, i), items));
347      } else if (type.isCollection()) {
348         ((Collection<?>)o).forEach(x -> l.add(toObject(partType, x, items)));
349      } else if (type.isStreamable()) {
350         forEachStreamableEntry(o, type, x -> l.add(toObject(partType, x, items)));
351      } else {
352         l.add(toObject(partType, o, items));
353      }
354      if (isSortCollections())
355         return sort(l);
356      return l;
357   }
358
359   private Map<String,Object> toMap(HttpPartType partType, ClassMeta<?> type, Object o, HttpPartSchema s) throws SerializeException, SchemaValidationException {
360      if (s == null)
361         s = DEFAULT_SCHEMA;
362      var m = new JsonMap();
363      if (type.isBean()) {
364         Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x);
365         HttpPartSchema s2 = s;
366         toBeanMap(o).forEachValue(checkNull, (pMeta, key, val, thrown) -> {
367            if (thrown == null)
368               m.put(key, toObject(partType, val, s2.getProperty(key)));
369         });
370      } else {
371         HttpPartSchema s2 = s;
372         ((Map<?,?>)o).forEach((k, v) -> m.put(s(k), toObject(partType, v, s2.getProperty(s(k)))));
373      }
374      if (isSortMaps())
375         return sort(m);
376      return m;
377   }
378
379   private Object toObject(HttpPartType partType, Object o, HttpPartSchema s) throws SerializeException, SchemaValidationException {
380      if (o == null)
381         return null;
382      if (s == null)
383         s = DEFAULT_SCHEMA;
384      ClassMeta cm = getClassMetaForObject(o);
385      HttpPartDataType t = s.getType(cm);
386      HttpPartFormat f = s.getFormat(cm);
387      HttpPartCollectionFormat cf = s.getCollectionFormat();
388
389      if (t == STRING) {
390         if (f == BYTE)
391            return base64Encode(toType(o, CM_ByteArray));
392         if (f == BINARY)
393            return toHex(toType(o, CM_ByteArray));
394         if (f == BINARY_SPACED)
395            return toSpacedHex(toType(o, CM_ByteArray));
396         if (f == DATE)
397            return toIsoDate(toType(o, CM_Calendar));
398         if (f == DATE_TIME)
399            return toIsoDateTime(toType(o, CM_Calendar));
400         return o;
401      } else if (t == ARRAY) {
402         List l = toList(partType, getClassMetaForObject(o), o, s);
403         if (cf == CSV)
404            return StringUtils.joine(l, ',');
405         if (cf == PIPES)
406            return StringUtils.joine(l, '|');
407         if (cf == SSV)
408            return StringUtils.join(l, ' ');
409         if (cf == TSV)
410            return StringUtils.join(l, '\t');
411         return l;
412      } else if (t == OBJECT) {
413         return toMap(partType, getClassMetaForObject(o), o, s);
414      }
415
416      return o;
417   }
418
419   private <T> T toType(Object in, ClassMeta<T> type) throws SerializeException {
420      try {
421         return convertToType(in, type);
422      } catch (InvalidDataConversionException e) {
423         throw new SerializeException(e);
424      }
425   }
426
427   @Override /* Overridden from Serializer */
428   protected void doWrite(SerializerPipe out, Object o) throws IOException, SerializeException {
429      try {
430         out.getWriter().write(write(HttpPartType.BODY, getSchema(), o));
431      } catch (SchemaValidationException e) {
432         throw new SerializeException(e);
433      }
434   }
435}