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