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.msgpack;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020import static org.apache.juneau.commons.utils.SystemUtils.*;
021
022import java.io.*;
023import java.time.*;
024import java.util.*;
025import java.util.concurrent.*;
026
027import org.apache.juneau.commons.bean.*;
028import org.apache.juneau.commons.collections.*;
029import org.apache.juneau.marshall.*;
030import org.apache.juneau.marshall.serializer.*;
031import org.apache.juneau.marshall.stream.*;
032
033/**
034 * Serializes POJO models to MessagePack.
035 *
036 * <h5 class='section'>Media types:</h5>
037 *
038 * Handles <c>Accept</c> types:  <bc>application/msgpack, octal/msgpack</bc>
039 * <p>
040 * Produces <c>Content-Type</c> types: <bc>application/msgpack</bc>
041 *
042 * <p>
043 * The legacy media type <bc>octal/msgpack</bc> is retained in the <c>Accept</c> list as a
044 * backward-compatibility alias. The <c>Content-Type</c> emitted by the serializer is always the
045 * RFC-standard <bc>application/msgpack</bc> value.
046 *
047 * <h5 class='topic'>Description</h5>
048 * <p>
049 * Java-to-MessagePack type mapping:
050 * <ul class='spaced-list'>
051 *    <li>Maps and beans → MessagePack map type
052 *    <li>Collections and arrays → MessagePack array type
053 *    <li>{@link String} / {@link Enum} → MessagePack string type
054 *    <li>{@link Boolean} → MessagePack boolean type
055 *    <li>{@link Number} (int, long, float, double, etc.) → MessagePack number type (variable-length int or float)
056 *    <li>{@link Date} / {@link Calendar} / {@link Instant} → ISO 8601 string
057 *    <li>{@link Duration} → ISO 8601 duration string
058 *    <li>{@code byte[]} → MessagePack binary type
059 *    <li>{@code null} → MessagePack nil type
060 *    <li>All other types → fallback to {@code toString()} as string
061 * </ul>
062 *
063 * <h5 class='section'>Notes:</h5><ul>
064 *    <li class='note'>This class is thread safe and reusable.
065 * </ul>
066 *
067 * <h5 class='section'>See Also:</h5><ul>
068 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/MessagePackSupport">MessagePack</a>
069
070 * </ul>
071 */
072@SuppressWarnings({
073   "java:S110", // Inheritance depth acceptable for this class hierarchy
074   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
075   "java:S9149", // Per-format static factories intentionally shadow the parent's.
076   "resource" // Closeable resources are owned by the caller's serializer session; Eclipse JDT @Owning warning is by design.
077})
078public class MsgPackSerializer extends OutputStreamSerializer implements MsgPackMetaProvider, TokenWritable, ArrayRecordWritable {
079
080   /** Default serializer, BASE64 string output. */
081   public static class Base64 extends MsgPackSerializer {
082
083      /**
084       * Constructor.
085       *
086       * @param builder The builder for this object.
087       *    <br>Cannot be <jk>null</jk>.
088       */
089      public Base64(Builder builder) {
090         super(reqnn("builder", builder).binaryFormat(BinaryFormat.BASE64));
091      }
092   }
093
094   /**
095    * Builder class.
096    */
097   public static class Builder extends OutputStreamSerializer.Builder<Builder> {
098
099      private static final Cache<HashKey,MsgPackSerializer> CACHE = Cache.of(HashKey.class, MsgPackSerializer.class).build();
100
101      private boolean addBeanTypesMsgPack;
102
103      /**
104       * Constructor, default settings.
105       */
106      protected Builder() {
107         produces("application/msgpack");
108         accept("application/msgpack,octal/msgpack");
109         addBeanTypesMsgPack = env("MsgPackSerializer.addBeanTypesMsgPack", false);
110      }
111
112      /**
113       * Copy constructor.
114       *
115       * @param copyFrom The builder to copy from.
116       *    <br>Cannot be <jk>null</jk>.
117       */
118      protected Builder(Builder copyFrom) {
119         super(reqnn("copyFrom", copyFrom));
120         addBeanTypesMsgPack = copyFrom.addBeanTypesMsgPack;
121      }
122
123      /**
124       * Copy constructor.
125       *
126       * @param copyFrom The bean to copy from.
127       *    <br>Cannot be <jk>null</jk>.
128       */
129      protected Builder(MsgPackSerializer copyFrom) {
130         super(reqnn("copyFrom", copyFrom));
131         addBeanTypesMsgPack = copyFrom.addBeanTypesMsgPack;
132      }
133
134      /**
135       * Add <js>"_type"</js> properties when needed.
136       *
137       * <p>
138       * If <jk>true</jk>, then <js>"_type"</js> properties will be added to beans if their type cannot be inferred
139       * through reflection.
140       *
141       * <p>
142       * When present, this value overrides the {@link org.apache.juneau.marshall.serializer.Serializer.Builder#addBeanTypes() Serializer.Builder#addBeanTypes()} setting and is
143       * provided to customize the behavior of specific serializers in a {@link SerializerSet}.
144       *
145       * @return This object.
146       */
147      public Builder addBeanTypesMsgPack() {
148         return addBeanTypesMsgPack(true);
149      }
150
151      /**
152       * Same as {@link #addBeanTypesMsgPack()} but allows you to explicitly specify the value.
153       *
154       * @param value The value for this setting.
155       * @return This object.
156       */
157      public Builder addBeanTypesMsgPack(boolean value) {
158         addBeanTypesMsgPack = value;
159         return this;
160      }
161
162      @Override /* Overridden from Context.Builder<?> */
163      public MsgPackSerializer build() {
164         return cache(CACHE).build(MsgPackSerializer.class);
165      }
166
167      @Override /* Overridden from Context.Builder<?> */
168      public Builder copy() {
169         return new Builder(this);
170      }
171
172      @Override /* Overridden from Context.Builder<?> */
173      public HashKey hashKey() {
174         return HashKey.of(super.hashKey(), addBeanTypesMsgPack);
175      }
176
177
178   }
179
180   /** Default serializer, spaced-hex string output. */
181   public static class SpacedHex extends MsgPackSerializer {
182
183      /**
184       * Constructor.
185       *
186       * @param builder The builder for this object.
187       */
188      public SpacedHex(Builder builder) {
189         super(builder.binaryFormat(BinaryFormat.SPACED_HEX));
190      }
191   }
192
193   /** Default serializer, all default settings.*/
194   public static final MsgPackSerializer DEFAULT = new MsgPackSerializer(create());
195   /** Default serializer, all default settings, spaced-hex string output.*/
196   public static final MsgPackSerializer DEFAULT_SPACED_HEX = new SpacedHex(create());
197
198   /** Default serializer, all default settings, spaced-hex string output.*/
199   public static final MsgPackSerializer DEFAULT_BASE64 = new Base64(create());
200
201   /**
202    * Creates a new builder for this object.
203    *
204    * @return A new builder.
205    */
206   public static Builder create() {
207      return new Builder();
208   }
209
210   protected final boolean addBeanTypesMsgPack;
211
212   private final Map<BeanPropertyMeta,MsgPackBeanPropertyMeta> msgPackBeanPropertyMetas = new ConcurrentHashMap<>();
213   private final Map<ClassMeta<?>,MsgPackClassMeta> msgPackClassMetas = new ConcurrentHashMap<>();
214
215   /**
216    * Constructor.
217    *
218    * @param builder The builder for this object.
219    */
220   public MsgPackSerializer(Builder builder) {
221      super(builder);
222      this.addBeanTypesMsgPack = builder.addBeanTypesMsgPack;
223   }
224
225   @Override /* Overridden from Context */
226   public Builder copy() {
227      return new Builder(this);
228   }
229
230   @Override /* Overridden from Context */
231   public MsgPackSerializerSession.Builder createSession() {
232      return MsgPackSerializerSession.create(this);
233   }
234
235   @Override /* Overridden from MsgPackMetaProvider */
236   public MsgPackBeanPropertyMeta getMsgPackBeanPropertyMeta(BeanPropertyMeta bpm) {
237      if (bpm == null)
238         return MsgPackBeanPropertyMeta.DEFAULT;
239      return msgPackBeanPropertyMetas.computeIfAbsent(bpm, k -> new MsgPackBeanPropertyMeta(k.getDelegateFor(), this));
240   }
241
242   @Override /* Overridden from MsgPackMetaProvider */
243   public MsgPackClassMeta getMsgPackClassMeta(ClassMeta<?> cm) {
244      return msgPackClassMetas.computeIfAbsent(cm, k -> new MsgPackClassMeta(k, this));
245   }
246
247   @Override /* Overridden from Context */
248   public MsgPackSerializerSession getSession() { return createSession().build(); }
249
250   /**
251    * Convenience delegator that opens a {@link MsgPackTokenWriter} over the output using
252    * <b>default session arguments</b> (mirrors {@link #write(Object)}).
253    *
254    * <p>
255    * The real implementation lives on {@link MsgPackSerializerSession#writeTokens(Object)}.
256    * Callers that need request-derived configuration (locale, timezone, schema, swaps) should
257    * call {@link #createSession()} and invoke
258    * {@link MsgPackSerializerSession#writeTokens(Object)} on the built session instead.
259    *
260    * @param output The output.
261    * @return A new {@link MsgPackTokenWriter}.
262    * @throws IOException If the output type is not supported or could not be opened.
263    */
264   @Override /* TokenWritable */
265   public TokenWriter writeTokens(Object output) throws IOException {
266      return getSession().writeTokens(output);
267   }
268
269   @Override
270   protected final boolean isAddBeanTypes() { return addBeanTypesMsgPack || super.isAddBeanTypes(); }
271
272   @Override /* Overridden from OutputStreamSerializer */
273   protected FluentMap<String,Object> properties() {
274      return super.properties()
275         .a("addBeanTypesMsgPack", addBeanTypesMsgPack);
276   }
277
278   /**
279    * Convenience delegator for the buffered array-element {@link RecordWriter} (uses default
280    * session args; see {@link #writeTokens(Object)}).  Real impl on
281    * {@link MsgPackSerializerSession#writeArrayRecords(Object)}.
282    *
283    * @param output The output.
284    * @return A buffered {@link RecordWriter}.
285    * @throws IOException If a problem occurred opening the underlying output.
286    */
287   @Override /* ArrayRecordWritable */
288   public RecordWriter writeArrayRecords(Object output) throws IOException {
289      return getSession().writeArrayRecords(output);
290   }
291
292   @Override /* ArrayRecordWritable */
293   public boolean isArrayRecordStreaming() { return false; }
294
295   /**
296    * Convenience delegator for the streaming, count-prefixed array-element {@link RecordWriter}
297    * (uses default session args; see {@link #writeTokens(Object)}).  Real impl on
298    * {@link MsgPackSerializerSession#writeArrayRecords(Object, int)}.
299    *
300    * @param output The output (must be an {@link OutputStream}).
301    * @param expectedCount The number of elements that will be written.
302    * @return A streaming {@link RecordWriter}.
303    * @throws IOException If a problem occurred opening the underlying output.
304    */
305   @Override /* ArrayRecordWritable */
306   public RecordWriter writeArrayRecords(Object output, int expectedCount) throws IOException {
307      return getSession().writeArrayRecords(output, expectedCount);
308   }
309}