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.json5l;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import org.apache.juneau.commons.collections.*;
022import org.apache.juneau.marshall.json.*;
023import org.apache.juneau.marshall.jsonl.*;
024
025/**
026 * Serializes POJO models to JSON5L (JSON5 Lines).
027 *
028 * <h5 class='topic'>Media types</h5>
029 * <p>
030 * Handles <c>Accept</c> types:  <bc>application/json5l, application/json5lines, text/json5l</bc>
031 * <p>
032 * Produces <c>Content-Type</c> types:  <bc>application/json5l</bc>
033 *
034 * <h5 class='topic'>Description</h5>
035 * <p>
036 * JSON5L combines the relaxed JSON5 dialect with JSONL's newline-delimited framing: each top-level
037 * value is written as a compact document on its own line, exactly as {@link JsonlSerializer} does.
038 * <p>
039 * By <b>default</b> the per-line output is strict RFC-8259 JSON (byte-identical to
040 * {@link JsonlSerializer}), keeping output maximally portable.  The {@link Builder#json5Sugar()
041 * json5Sugar()} opt-in switches the per-line output to JSON5 sugar (single-quoted strings, unquoted
042 * field names where safe).
043 *
044 * <h5 class='section'>Example:</h5>
045 * <p class='bjava'>
046 *    <jc>// Strict-per-line output (default)</jc>
047 *    String <jv>json5l</jv> = Json5lSerializer.<jsf>DEFAULT</jsf>.write(<jv>myList</jv>);
048 *
049 *    <jc>// JSON5-sugar-per-line output</jc>
050 *    Json5lSerializer <jv>serializer</jv> = Json5lSerializer.<jsm>create</jsm>().json5Sugar().build();
051 *    String <jv>json5l</jv> = <jv>serializer</jv>.write(<jv>myList</jv>);
052 * </p>
053 *
054 * <h5 class='figure'>Example output, sugar off (List of beans):</h5>
055 * <p class='bjson'>
056 * {"name":"Alice","age":30}
057 * {"name":"Bob","age":25}
058 * </p>
059 *
060 * <h5 class='figure'>Example output, sugar on:</h5>
061 * <p class='bjson'>
062 * {name:'Alice',age:30}
063 * {name:'Bob',age:25}
064 * </p>
065 *
066 * <h5 class='section'>Notes:</h5><ul>
067 *    <li class='note'>This class is thread safe and reusable.
068 * </ul>
069 *
070 * <h5 class='section'>See Also:</h5><ul>
071 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Json5l">JSON5L Basics</a>
072 * </ul>
073 */
074@SuppressWarnings({
075   "java:S110", // Inheritance depth acceptable
076   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
077   "java:S9149" // Per-format static factories intentionally shadow the parent's.
078})
079public class Json5lSerializer extends JsonlSerializer {
080
081   /**
082    * Builder class.
083    */
084   public static class Builder extends JsonSerializer.Builder<Builder> {
085
086      private static final Cache<HashKey,Json5lSerializer> CACHE = Cache.of(HashKey.class, Json5lSerializer.class).build();
087
088      boolean json5Sugar;
089
090      /**
091       * Constructor, default settings.
092       */
093      protected Builder() {
094         produces("application/json5l")
095            .accept("application/json5l,application/json5lines,text/json5l,application/jsonl;q=0.9,application/jsonlines;q=0.9,application/x-ndjson;q=0.9,text/jsonl;q=0.9")
096            .type(Json5lSerializer.class)
097            .useWhitespace(false);
098      }
099
100      /**
101       * Copy constructor.
102       *
103       * @param copyFrom The builder to copy from.
104       *    <br>Cannot be <jk>null</jk>.
105       */
106      protected Builder(Builder copyFrom) {
107         super(reqnn("copyFrom", copyFrom));
108         json5Sugar = copyFrom.json5Sugar;
109      }
110
111      /**
112       * Copy constructor.
113       *
114       * @param copyFrom The bean to copy from.
115       *    <br>Cannot be <jk>null</jk>.
116       */
117      protected Builder(Json5lSerializer copyFrom) {
118         super(reqnn("copyFrom", copyFrom));
119         json5Sugar = copyFrom.json5Sugar;
120      }
121
122      /**
123       * Emit JSON5 sugar (single-quoted strings, unquoted field names where safe) on each line
124       * instead of strict RFC-8259 JSON.
125       *
126       * <p>
127       * Off by default, in which case the per-line output is byte-identical to
128       * {@link JsonlSerializer}.
129       *
130       * @return This object.
131       */
132      public Builder json5Sugar() {
133         json5Sugar = true;
134         return this;
135      }
136
137      @Override /* Overridden from Context.Builder<?> */
138      public HashKey hashKey() {
139         return HashKey.of(super.hashKey(), json5Sugar);
140      }
141
142      @Override /* Overridden from Context.Builder<?> */
143      public Json5lSerializer build() {
144         return cache(CACHE).build(Json5lSerializer.class);
145      }
146
147      @Override /* Overridden from Context.Builder<?> */
148      public Builder copy() {
149         return new Builder(this);
150      }
151   }
152
153   /** Default serializer, strict-JSON-per-line. */
154   public static final Json5lSerializer DEFAULT = new Json5lSerializer(create());
155
156   final boolean json5Sugar;
157
158   /**
159    * Creates a new builder for this object.
160    *
161    * @return A new builder.
162    */
163   public static Builder create() {
164      return new Builder();
165   }
166
167   /**
168    * Constructor.
169    *
170    * @param builder The builder for this object.
171    */
172   public Json5lSerializer(Builder builder) {
173      super(builder.useWhitespace(false));
174      json5Sugar = builder.json5Sugar;
175   }
176
177   /**
178    * Returns <jk>true</jk> if this serializer emits JSON5 sugar on each line.
179    *
180    * @return <jk>true</jk> if JSON5 sugar is enabled.
181    */
182   public boolean isJson5Sugar() {
183      return json5Sugar;
184   }
185
186   @Override /* Overridden from Context */
187   public Builder copy() {
188      return new Builder(this);
189   }
190
191   @Override /* Overridden from Context */
192   public Json5lSerializerSession.Builder createSession() {
193      return Json5lSerializerSession.create(this);
194   }
195
196   @Override /* Overridden from Context */
197   public Json5lSerializerSession getSession() {
198      return createSession().build();
199   }
200}