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.markdown;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.io.*;
022import java.util.*;
023import java.util.concurrent.*;
024
025import org.apache.juneau.commons.bean.*;
026import org.apache.juneau.commons.collections.*;
027import org.apache.juneau.marshall.*;
028import org.apache.juneau.marshall.serializer.*;
029import org.apache.juneau.marshall.stream.*;
030
031/**
032 * Serializes POJO models to Markdown (fragment mode: tables and lists).
033 *
034 * <h5 class='topic'>Media types</h5>
035 * <p>
036 * Handles <c>Accept</c> types:  <bc>text/markdown, text/x-markdown</bc>
037 * <p>
038 * Produces <c>Content-Type</c> types:  <bc>text/markdown</bc>
039 *
040 * <h5 class='topic'>Description</h5>
041 * <p>
042 * The conversion is as follows:
043 * <ul class='spaced-list'>
044 *    <li>Beans and Maps are serialized as 2-column key/value Markdown tables.
045 *    <li>Collections and arrays of beans/maps with uniform structure are serialized as multi-column Markdown tables.
046 *    <li>Collections and arrays of simple values are serialized as Markdown bulleted lists.
047 *    <li>Nested beans and complex values in table cells are serialized as inline JSON5 wrapped in backticks.
048 *    <li>Null values are rendered as <js>"*null*"</js> (italic) by default.
049 * </ul>
050 *
051 * <h5 class='section'>Example:</h5>
052 * <p class='bjava'>
053 *    <jc>// Use the default serializer to serialize a bean</jc>
054 *    String <jv>md</jv> = MarkdownSerializer.<jsf>DEFAULT</jsf>.write(<jv>someObject</jv>);
055 *
056 *    <jc>// Create a custom serializer</jc>
057 *    MarkdownSerializer <jv>serializer</jv> = MarkdownSerializer.<jsm>create</jsm>().nullValue(<js>"—"</js>).build();
058 *
059 *    <jc>// Serialize a List of beans to a multi-column Markdown table</jc>
060 *    String <jv>md</jv> = <jv>serializer</jv>.write(<jv>listOfBeans</jv>);
061 * </p>
062 *
063 * <h5 class='figure'>Example output (bean):</h5>
064 * <p class='bcode'>
065 *    | Property | Value |
066 *    |---|---|
067 *    | name | Alice |
068 *    | age | 30 |
069 * </p>
070 *
071 * <h5 class='section'>Limitations:</h5><ul>
072 *    <li class='note'>Multi-line string values cannot round-trip; newlines are replaced with {@code <br>}.
073 *    <li class='note'>The configured null value string (default: {@code *null*}) cannot be stored as a literal string value.
074 *    <li class='note'>This serializer is thread safe and reusable.
075 * </ul>
076 *
077 * <h5 class='section'>See Also:</h5><ul>
078 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Markdown">Markdown</a>
079 * </ul>
080 */
081@SuppressWarnings({
082   "java:S110", // Inheritance depth acceptable for this class hierarchy
083   "java:S115", // Constants use UPPER_snakeCase naming convention
084   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
085   "java:S9149", // Per-format static factories intentionally shadow the parent's.
086   "resource" // Closeable resources are owned by the caller's serializer session; Eclipse JDT @Owning warning is by design.
087})
088public class MarkdownSerializer extends WriterSerializer implements MarkdownMetaProvider, RecordWritable {
089
090   /**
091    * Builder class.
092    */
093   @SuppressWarnings({
094      "java:S119" // 'SELF' (CRTP self-type) is intentional and clearer than a single-letter name.
095   })
096   public abstract static class Builder<SELF extends Builder<SELF>> extends WriterSerializer.Builder<SELF> {
097
098      private static final Cache<HashKey,MarkdownSerializer> CACHE = Cache.of(HashKey.class, MarkdownSerializer.class).build();
099
100      String nullValue;
101      boolean showHeaders;
102
103      /**
104       * Constructor, default settings.
105       */
106      protected Builder() {
107         produces("text/markdown");
108         accept("text/markdown,text/x-markdown");
109         nullValue = "*null*";
110         showHeaders = true;
111      }
112
113      /**
114       * Copy constructor.
115       *
116       * @param copyFrom The builder to copy from.
117       *    <br>Cannot be <jk>null</jk>.
118       */
119      protected Builder(Builder<?> copyFrom) {
120         super(reqnn("copyFrom", copyFrom));
121         nullValue = copyFrom.nullValue;
122         showHeaders = copyFrom.showHeaders;
123      }
124
125      /**
126       * Copy constructor.
127       *
128       * @param copyFrom The bean to copy from.
129       *    <br>Cannot be <jk>null</jk>.
130       */
131      protected Builder(MarkdownSerializer copyFrom) {
132         super(reqnn("copyFrom", copyFrom));
133         nullValue = copyFrom.nullValue;
134         showHeaders = copyFrom.showHeaders;
135      }
136
137      /**
138       * The string to render for null values.
139       *
140       * <p>
141       * Default is {@code *null*} (rendered as Markdown italic).
142       *
143       * @param value The null marker string.
144       * @return This object.
145       */
146      public SELF nullValue(String value) {
147         nullValue = value == null ? "*null*" : value;
148         return self();
149      }
150
151      /**
152       * Whether to show table header rows.
153       *
154       * <p>
155       * Default is <jk>true</jk>.
156       *
157       * @param value Whether to show headers.
158       * @return This object.
159       */
160      public SELF showHeaders(boolean value) {
161         showHeaders = value;
162         return self();
163      }
164
165      @Override /* Overridden from Context.Builder<?> */
166      public HashKey hashKey() {
167         return HashKey.of(super.hashKey(), nullValue, showHeaders);
168      }
169
170      @Override /* Overridden from Context.Builder<?> */
171      public MarkdownSerializer build() {
172         return cache(CACHE).build(MarkdownSerializer.class);
173      }
174
175      @Override /* Overridden from Context.Builder<?> */
176      public abstract SELF copy();
177
178
179   }
180
181   /**
182    * Concrete default builder leaf for the non-subclassed {@link MarkdownSerializer#create()} / {@link MarkdownSerializer#copy()} path.
183    */
184   public static final class DefaultBuilder extends Builder<DefaultBuilder> {
185
186      DefaultBuilder() {}
187
188      DefaultBuilder(MarkdownSerializer copyFrom) {
189         super(copyFrom);
190      }
191
192      DefaultBuilder(Builder<?> copyFrom) {
193         super(copyFrom);
194      }
195
196      @Override /* Overridden from Context.Builder<?> */
197      public DefaultBuilder copy() {
198         return new DefaultBuilder(this);
199      }
200   }
201
202   /**
203    * Default serializer, all default settings.
204    */
205   public static final MarkdownSerializer DEFAULT = new MarkdownSerializer(create());
206
207   /**
208    * Creates a new builder for this object.
209    *
210    * @return A new builder.
211    */
212   @SuppressWarnings({
213      "java:S1452" // Builder<?> wildcard return intentional; callers chain via fluent API without needing the concrete type
214   })
215   public static Builder<?> create() {
216      return new DefaultBuilder();
217   }
218
219   private final Map<ClassMeta<?>,MarkdownClassMeta> markdownClassMetas = new ConcurrentHashMap<>();
220   private final Map<BeanPropertyMeta,MarkdownBeanPropertyMeta> markdownBeanPropertyMetas = new ConcurrentHashMap<>();
221   final String nullValue;
222   final boolean showHeaders;
223
224   /**
225    * Constructor.
226    *
227    * @param builder The builder for this object.
228    */
229   public MarkdownSerializer(Builder<?> builder) {
230      super(builder);
231      nullValue = or(builder.nullValue, "*null*");
232      showHeaders = builder.showHeaders;
233   }
234
235   /**
236    * Returns the string rendered for null values.
237    *
238    * @return The null marker.
239    */
240   public String getNullValue() {
241      return nullValue;
242   }
243
244   /**
245    * Returns whether table headers are shown.
246    *
247    * @return Whether headers are shown.
248    */
249   public boolean isShowHeaders() {
250      return showHeaders;
251   }
252
253   @Override /* Overridden from Context */
254   public Builder<?> copy() {
255      return new DefaultBuilder(this);
256   }
257
258   @Override /* Overridden from Context */
259   public MarkdownSerializerSession.Builder<?> createSession() {
260      return MarkdownSerializerSession.create(this);
261   }
262
263   @Override /* Overridden from MarkdownMetaProvider */
264   public MarkdownBeanPropertyMeta getMarkdownBeanPropertyMeta(BeanPropertyMeta bpm) {
265      if (bpm == null)
266         return MarkdownBeanPropertyMeta.DEFAULT;
267      return markdownBeanPropertyMetas.computeIfAbsent(bpm, k -> new MarkdownBeanPropertyMeta(k.getDelegateFor(), this));
268   }
269
270   @Override /* Overridden from MarkdownMetaProvider */
271   public MarkdownClassMeta getMarkdownClassMeta(ClassMeta<?> cm) {
272      return markdownClassMetas.computeIfAbsent(cm, k -> new MarkdownClassMeta(k, this));
273   }
274
275   @Override /* Overridden from Context */
276   public MarkdownSerializerSession getSession() { return createSession().build(); }
277
278   /**
279    * Convenience delegator that opens a {@link RecordWriter} over the output using
280    * <b>default session arguments</b> (mirrors {@link #write(Object)}).
281    *
282    * <p>
283    * The real implementation lives on {@link MarkdownSerializerSession#writeRecords(Object)}.
284    * Callers that need request-derived configuration should call {@link #createSession()} and
285    * invoke {@link MarkdownSerializerSession#writeRecords(Object)} on the built session instead.
286    *
287    * @param output The output.
288    * @return A new {@link RecordWriter}.
289    * @throws IOException If a problem occurred opening the underlying output.
290    */
291   @Override /* RecordWritable */
292   public RecordWriter writeRecords(Object output) throws IOException {
293      return getSession().writeRecords(output);
294   }
295
296   @Override /* RecordWritable */
297   public boolean isRecordStreaming() {
298      return false;
299   }
300}