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.uon;
018
019import static org.apache.juneau.commons.utils.IoUtils.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021import static org.apache.juneau.commons.utils.ThrowableUtils.*;
022
023import java.io.*;
024import java.time.*;
025import java.time.temporal.*;
026import java.util.*;
027import java.util.function.*;
028
029import org.apache.juneau.commons.bean.*;
030import org.apache.juneau.commons.conversion.*;
031import org.apache.juneau.commons.httppart.*;
032import org.apache.juneau.commons.lang.*;
033import org.apache.juneau.marshall.*;
034import org.apache.juneau.marshall.httppart.*;
035import org.apache.juneau.marshall.serializer.*;
036import org.apache.juneau.marshall.stream.*;
037
038/**
039 * Session object that lives for the duration of a single use of {@link UonSerializer}.
040 *
041 * <h5 class='section'>Notes:</h5><ul>
042 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
043 * </ul>
044 *
045 * <h5 class='section'>See Also:</h5><ul>
046 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/UonSupport">UON</a>
047
048 * </ul>
049 */
050@SuppressWarnings({
051   "java:S110", // Inheritance depth acceptable for this class hierarchy
052   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
053   "rawtypes", // writeCollection(), writeMap() and the Map/Collection/BeanMap casts in the dispatch use raw types because the object was already matched by runtime kind
054   "resource", // Resource management handled externally
055   "unchecked" // Type erasure requires unchecked casts in collection/map serialization
056})
057public class UonSerializerSession extends WriterSerializerSession implements HttpPartSerializerSession, RecordWritable {
058
059   /**
060    * Builder class.
061    */
062   @SuppressWarnings({
063      "java:S119" // 'SELF' (CRTP self-type) is intentional and clearer than a single-letter name.
064   })
065   public abstract static class Builder<SELF extends Builder<SELF>> extends WriterSerializerSession.Builder<SELF> {
066
067      private boolean encoding;
068      private ParamFormat paramFormat;
069
070      /**
071       * Constructor
072       *
073       * @param ctx The context creating this session.
074       *    <br>Cannot be <jk>null</jk>.
075       */
076      protected Builder(UonSerializer ctx) {
077         super(reqnn("ctx", ctx));
078         encoding = ctx.isEncoding();
079         paramFormat = ctx.getParamFormat();
080      }
081
082      @Override
083      public UonSerializerSession build() {
084         return new UonSerializerSession(this);
085      }
086
087      /**
088       * Encode non-valid URI characters.
089       *
090       * @param value The new value for this setting.
091       * @return This object.
092       */
093      public SELF encoding(boolean value) {
094         encoding = value;
095         return self();
096      }
097
098      /**
099       * Format to use for query/form-data/header values.
100       *
101       * @param value The new value for this setting.
102       * @return This object.
103       */
104      public SELF paramFormat(ParamFormat value) {
105         paramFormat = value;
106         return self();
107      }
108
109      @Override /* Overridden from Builder */
110      public SELF property(String key, Object value) {
111         if (key == null) { super.property(key, value); return self(); }
112         switch (key) {
113            case "encoding", "UonSerializerSession.encoding":
114               return encoding(cvt(value, Boolean.class));
115            case "paramFormat", "UonSerializerSession.paramFormat":
116               return paramFormat(cvt(value, ParamFormat.class));
117            default:
118               super.property(key, value);
119               return self();
120         }
121      }
122
123   }
124
125   /**
126    * Concrete default builder leaf for the non-subclassed {@code create()} path (CRTP terminal).
127    */
128   public static final class DefaultBuilder extends Builder<DefaultBuilder> {
129
130      DefaultBuilder(UonSerializer ctx) {
131         super(ctx);
132      }
133   }
134
135   /**
136    * Creates a new builder for this object.
137    *
138    * @param ctx The context creating this session.
139    *    <br>Cannot be <jk>null</jk>.
140    * @return A new builder.
141    */
142   @SuppressWarnings({
143      "java:S1452" // Builder<?> wildcard return intentional; callers use it to construct session instances polymorphically
144   })
145   public static Builder<?> create(UonSerializer ctx) {
146      return new DefaultBuilder(reqnn("ctx", ctx));
147   }
148
149   private final boolean encoding;
150   private final ParamFormat paramFormat;
151   private final boolean plainTextParams;
152
153   /**
154    * Constructor.
155    *
156    * @param builder The builder for this object.
157    */
158   public UonSerializerSession(Builder<?> builder) {
159      super(builder);
160      encoding = builder.encoding;
161      paramFormat = builder.paramFormat;
162      plainTextParams = paramFormat == ParamFormat.PLAINTEXT;
163   }
164
165   @Override /* RecordWritable */
166   public RecordWriter writeRecords(Object output) throws IOException {
167      return RecordAdapter.writer(this, output);
168   }
169
170   @Override /* RecordWritable */
171   public boolean isRecordStreaming() {
172      return false;
173   }
174
175   @Override /* Overridden from HttpPartSerializer */
176   public String write(HttpPartType type, HttpPartSchema schema, Object value) throws SerializeException, SchemaValidationException {
177      try {
178         // Shortcut for simple types.
179         var cm = getClassMetaForObject(value);
180         if (nn(cm) && (schema == null || schema.getType() == HttpPartDataType.NO_TYPE)) {
181            if (cm.isNumber() || cm.isBoolean())
182               return BasicConverter.INSTANCE.to(value, String.class);
183            if (cm.isString()) {
184               var s = BasicConverter.INSTANCE.to(value, String.class);
185               if (s.isEmpty() || ! UonUtils.needsQuotes(s))
186                  return s;
187            }
188         }
189         var w = new StringWriter();
190         writeAnything(getUonWriter(w).i(getInitialDepth()), value, getExpectedRootType(value), "root", null);
191         return w.toString();
192      } catch (Exception e) {
193         throw toRex(e);
194      }
195   }
196
197   private final UonWriter getUonWriter(Writer out) {
198      return new UonWriter(this, out, isUseWhitespace(), getMaxIndent(), isEncoding(), isTrimStrings(), plainTextParams, getQuoteChar(), getUriResolver());
199   }
200
201   private SerializerWriter<?> writeBeanMap(UonWriter out, BeanMap<?> m, String typeName) throws SerializeException {
202
203      if (! plainTextParams)
204         out.append('(');
205
206      var addComma = Flag.create();
207
208      if (nn(typeName)) {
209         var pm = m.getMeta().getTypeProperty();
210         out.cr(indent).appendObject(pm.getName(), false).append('=').appendObject(typeName, false);
211         addComma.set();
212      }
213
214      var checkNull = (Predicate<Object>)(x -> isKeepNullProperties() || nn(x));
215      m.forEachValue(checkNull, (pMeta, key, value, thrown) -> {
216         var cMeta = (ClassMeta<?>) pMeta.getBeanInfo();
217
218         if (nn(thrown))
219            onBeanGetterException(pMeta, thrown);
220
221         if (canIgnoreValue(pMeta, key, value))
222            return;
223
224         addComma.ifSet(() -> out.append(',')).set();
225
226         out.cr(indent).appendObject(key, false).append('=');
227
228         writeAnything(out, value, cMeta, key, pMeta);
229      });
230
231      if (m.size() > 0)
232         out.cre(indent - 1);
233      if (! plainTextParams)
234         out.append(')');
235
236      return out;
237   }
238
239   private SerializerWriter<?> writeCollection(UonWriter out, Collection c, ClassMeta<?> type) throws SerializeException {
240
241      var elementType = type.getElementType();
242
243      if (! plainTextParams)
244         out.append('@').append('(');
245
246      var addComma = Flag.create();
247      forEachEntry(c, x -> {
248         addComma.ifSet(() -> out.append(',')).set();
249         out.cr(indent);
250         writeAnything(out, x, elementType, "<iterator>", null);
251      });
252
253      addComma.ifSet(() -> out.cre(indent - 1));
254      if (! plainTextParams)
255         out.append(')');
256
257      return out;
258   }
259
260   private SerializerWriter<?> writeStreamable(UonWriter out, Object o, ClassMeta<?> sType, ClassMeta<?> type) throws SerializeException {
261
262      var elementType = type.getElementType();
263
264      if (! plainTextParams)
265         out.append('@').append('(');
266
267      var addComma = Flag.create();
268      forEachStreamableEntry(o, sType, x -> {
269         addComma.ifSet(() -> out.append(',')).set();
270         out.cr(indent);
271         writeAnything(out, x, elementType, "<iterator>", null);
272      });
273
274      addComma.ifSet(() -> out.cre(indent - 1));
275      if (! plainTextParams)
276         out.append(')');
277
278      return out;
279   }
280
281   private SerializerWriter<?> writeMap(UonWriter out, Map m, ClassMeta<?> type) throws SerializeException {
282
283      var keyType = type.getKeyType();
284      var valueType = type.getValueType();
285
286      if (! plainTextParams)
287         out.append('(');
288
289      var addComma = Flag.create();
290      forEachEntry(m, x -> {
291         addComma.ifSet(() -> out.append(',')).set();
292         var value = x.getValue();
293         var key = generalize(x.getKey(), keyType);
294         out.cr(indent).appendObject(key, false).append('=');
295         writeAnything(out, value, valueType, toString(key), null);
296      });
297
298      addComma.ifSet(() -> out.cre(indent - 1));
299
300      if (! plainTextParams)
301         out.append(')');
302
303      return out;
304   }
305
306   @Override /* Overridden from Serializer */
307   protected void doWrite(SerializerPipe out, Object o) throws IOException, SerializeException {
308      writeAnything(getUonWriter(out).i(getInitialDepth()), o, getExpectedRootType(o), "root", null);
309   }
310
311   /**
312    * Format to use for query/form-data/header values.
313    *
314    * @see UonSerializer.Builder#paramFormat(ParamFormat)
315    * @return
316    *    Specifies the format to use for URL GET parameter keys and values.
317    */
318   protected final ParamFormat getParamFormat() { return paramFormat; }
319
320   /**
321    * Converts the specified output target object to an {@link UonWriter}.
322    *
323    * @param out The output target object.
324    *    <br>Must not be <jk>null</jk>.
325    * @return The output target object wrapped in an {@link UonWriter}.
326    */
327   protected final UonWriter getUonWriter(SerializerPipe out) {
328      var output = out.getRawOutput();
329      if (output instanceof UonWriter output2)
330         return output2;
331      var w = new UonWriter(this, out.getWriter(), isUseWhitespace(), getMaxIndent(), isEncoding(), isTrimStrings(), plainTextParams, getQuoteChar(), getUriResolver());
332      out.setWriter(w);
333      return w;
334   }
335
336   @Override
337   public final boolean isAddBeanTypes() { return super.isAddBeanTypes(); }
338
339   /**
340    * Encode non-valid URI characters.
341    *
342    * @see UonSerializer.Builder#encoding()
343    * @return
344    *    <jk>true</jk> if non-valid URI characters should be encoded with <js>"%xx"</js> constructs.
345    */
346   protected final boolean isEncoding() { return encoding; }
347
348   /**
349    * Workhorse method.
350    *
351    * <p>
352    * Determines the type of object, and then calls the appropriate type-specific serialization method.
353    *
354    * @param out The writer to serialize to.
355    *    <br>Must not be <jk>null</jk>.
356    * @param o The object being serialized.
357    *    <br>Can be <jk>null</jk> (written as the UON null literal).
358    * @param eType The expected type of the object if this is a bean property.
359    *    <br>Can be <jk>null</jk> (defaults to {@code Object}).
360    * @param attrName
361    *    The bean property name if this is a bean property.
362    *    <jk>null</jk> if this isn't a bean property being serialized.
363    * @param pMeta The bean property metadata.
364    *    <br>Can be <jk>null</jk> (bean-property-level URI detection is skipped).
365    * @return The same writer passed in.
366    * @throws SerializeException Generic serialization error occurred.
367    */
368   @SuppressWarnings({
369      "java:S3776" // Cognitive complexity acceptable for this specific logic
370   })
371   protected UonWriter writeAnything(UonWriter out, Object o, ClassMeta<?> eType, String attrName, BeanPropertyMeta pMeta) throws SerializeException {
372
373      if (o == null) {
374         out.appendObject(null, false);
375         return out;
376      }
377
378      if (eType == null)
379         eType = object();
380
381      ClassMeta<?> aType = null;       // The actual type
382      ClassMeta<?> sType = null;       // The serialized type
383
384      aType = push2(attrName, o, eType);
385      boolean isRecursion = aType == null;
386
387      // Handle recursion
388      if (aType == null) {
389         o = null;
390         aType = object();
391      }
392
393      // Handle Optional<X>
394      if (isOptional(aType)) {
395         o = getOptionalValue(o);
396         eType = getOptionalType(eType);
397         aType = getClassMetaForObject(o, object());
398      }
399
400      sType = aType;
401      var typeName = getBeanTypeName(this, eType, aType, pMeta);
402
403      // Swap if necessary
404      var swap = aType.getSwap(this);
405      if (nn(swap)) {
406         o = swap(swap, o);
407         sType = swap.getSwapClassMeta(this);
408
409         // If the getSwapClass() method returns Object, we need to figure out
410         // the actual type now.
411         if (sType.isObject())
412            sType = getClassMetaForObject(o);
413      }
414
415      // '\0' characters are considered null.
416      if (o == null || (sType.isChar() && ((Character)o).charValue() == 0))
417         out.appendObject(null, false);
418      else if (sType.isBean())
419         writeBeanMap(out, toBeanMap(o), typeName);
420      else if (sType.isMap()) {
421         if (sType.isBeanMap())
422            writeBeanMap(out, (BeanMap)o, typeName);
423         else
424            writeMap(out, (Map)o, eType);
425      } else if (sType.isCollection()) {
426         writeCollection(out, (Collection)o, eType);
427      } else if (sType.isArray()) {
428         writeCollection(out, toList(sType.inner(), o), eType);
429      } else if (sType.isBoolean()) {
430         out.appendBoolean(o);
431      } else if (sType.isNumber()) {
432         out.appendNumber(o);
433      } else if (sType.isUri() || (nn(pMeta) && pMeta.isUri())) {
434         out.appendUri(o);
435      } else if (sType.isDate()) {
436         out.appendObject(writeDate((Date)o, sType), false);
437      } else if (sType.isCalendar()) {
438         out.appendObject(writeCalendar(o, sType), false);
439      } else if (sType.isTemporal()) {
440         out.appendObject(writeTemporal((TemporalAccessor)o, sType), false);
441      } else if (sType.isDuration()) {
442         out.appendObject(writeDuration((Duration)o), false);
443      } else if (sType.isPeriod()) {
444         out.appendObject(writePeriod((Period)o), false);
445      } else if (sType.isStreamable()) {
446         writeStreamable(out, o, sType, eType);
447      } else if (sType.isReader()) {
448         pipe((Reader)o, out, SerializerSession::handleThrown);
449      } else if (sType.isInputStream()) {
450         pipe((InputStream)o, out, SerializerSession::handleThrown);
451      } else {
452         out.appendObject(o, false);
453      }
454
455      if (! isRecursion)
456         pop();
457      return out;
458   }
459}