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 org.apache.juneau.commons.collections.*;
022
023/**
024 * Serializes POJO models to Markdown document mode (headings + tables).
025 *
026 * <h5 class='topic'>Media types</h5>
027 * <p>
028 * Handles <c>Accept</c> types:  <bc>text/markdown, text/x-markdown</bc>
029 * <p>
030 * Produces <c>Content-Type</c> types:  <bc>text/markdown</bc>
031 *
032 * <h5 class='topic'>Description</h5>
033 * <p>
034 * Extends {@link MarkdownSerializer} to produce full Markdown documents with heading-based structure.
035 * Simple bean properties are rendered in a key/value table under the top-level heading.
036 * Nested bean and map properties are rendered as sub-sections with their own headings.
037 *
038 * <h5 class='section'>Example:</h5>
039 * <p class='bjava'>
040 *    <jc>// Serialize a bean to a Markdown document</jc>
041 *    String <jv>md</jv> = MarkdownDocSerializer.<jsf>DEFAULT</jsf>.write(<jv>someBean</jv>);
042 *
043 *    <jc>// Create a custom doc serializer with a document title</jc>
044 *    MarkdownDocSerializer <jv>s</jv> = MarkdownDocSerializer.<jsm>create</jsm>().title(<js>"My Report"</js>).build();
045 *    String <jv>md</jv> = <jv>s</jv>.write(<jv>someBean</jv>);
046 * </p>
047 *
048 * <h5 class='figure'>Example output (nested bean):</h5>
049 * <p class='bcode'>
050 *    # Person
051 *
052 *    | Property | Value |
053 *    |---|---|
054 *    | name | Alice |
055 *    | age | 30 |
056 *
057 *    ## address
058 *
059 *    | Property | Value |
060 *    |---|---|
061 *    | city | Boston |
062 *    | state | MA |
063 * </p>
064 *
065 * <h5 class='section'>Limitations:</h5><ul>
066 *    <li class='note'>Multi-line string values cannot round-trip; newlines are replaced with {@code <br>}.
067 *    <li class='note'>The configured null value string (default: {@code *null*}) cannot be stored as a literal string value.
068 *    <li class='note'>Document mode output requires {@link MarkdownDocParser} for parsing; heading text must match property names exactly.
069 *    <li class='note'>This serializer is thread safe and reusable.
070 * </ul>
071 *
072 * <h5 class='section'>See Also:</h5><ul>
073 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Markdown">Markdown</a>
074 * </ul>
075 */
076@SuppressWarnings({
077   "java:S110", // Inheritance depth acceptable for this class hierarchy
078   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
079   "java:S9149" // Per-format static factories intentionally shadow the parent's.
080})
081public class MarkdownDocSerializer extends MarkdownSerializer {
082
083   /**
084    * Builder class.
085    */
086   public static class Builder extends MarkdownSerializer.Builder<Builder> {
087
088      private static final Cache<HashKey,MarkdownDocSerializer> CACHE = Cache.of(HashKey.class, MarkdownDocSerializer.class).build();
089
090      String title;
091      int headingLevel;
092      boolean addHorizontalRules;
093      String headerContent;
094      String footerContent;
095
096      /**
097       * Constructor, default settings.
098       */
099      protected Builder() {
100         headingLevel = 1;
101         type(MarkdownDocSerializer.class);
102      }
103
104      /**
105       * Copy constructor.
106       *
107       * @param copyFrom The builder to copy from.
108       *    <br>Cannot be <jk>null</jk>.
109       */
110      protected Builder(Builder copyFrom) {
111         super(reqnn("copyFrom", copyFrom));
112         title = copyFrom.title;
113         headingLevel = copyFrom.headingLevel;
114         addHorizontalRules = copyFrom.addHorizontalRules;
115         headerContent = copyFrom.headerContent;
116         footerContent = copyFrom.footerContent;
117      }
118
119      /**
120       * Copy constructor.
121       *
122       * @param copyFrom The bean to copy from.
123       *    <br>Cannot be <jk>null</jk>.
124       */
125      protected Builder(MarkdownDocSerializer copyFrom) {
126         super(reqnn("copyFrom", copyFrom));
127         title = copyFrom.title;
128         headingLevel = copyFrom.headingLevel;
129         addHorizontalRules = copyFrom.addHorizontalRules;
130         headerContent = copyFrom.headerContent;
131         footerContent = copyFrom.footerContent;
132      }
133
134      /**
135       * The document title (rendered as the top-level heading).
136       *
137       * @param value The title.
138       * @return This object.
139       */
140      public Builder title(String value) {
141         title = value;
142         return this;
143      }
144
145      /**
146       * The starting heading level (1-6).
147       *
148       * <p>
149       * Default is 1 ({@code # Heading}).
150       *
151       * @param value The heading level.
152       * @return This object.
153       */
154      public Builder headingLevel(int value) {
155         headingLevel = Math.min(6, Math.max(1, value));
156         return this;
157      }
158
159      /**
160       * Whether to add horizontal rules ({@code ---}) between sections.
161       *
162       * @param value Whether to add horizontal rules.
163       * @return This object.
164       */
165      public Builder addHorizontalRules(boolean value) {
166         addHorizontalRules = value;
167         return this;
168      }
169
170      /**
171       * Static content prepended to the document output (e.g. YAML front matter).
172       *
173       * @param value The header content.
174       * @return This object.
175       */
176      public Builder headerContent(String value) {
177         headerContent = value;
178         return this;
179      }
180
181      /**
182       * Static content appended to the document output.
183       *
184       * @param value The footer content.
185       * @return This object.
186       */
187      public Builder footerContent(String value) {
188         footerContent = value;
189         return this;
190      }
191
192      @Override /* Overridden from Context.Builder<?> */
193      public HashKey hashKey() {
194         return HashKey.of(super.hashKey(), title, headingLevel, addHorizontalRules, headerContent, footerContent);
195      }
196
197      @Override /* Overridden from Context.Builder<?> */
198      public MarkdownDocSerializer build() {
199         return cache(CACHE).build(MarkdownDocSerializer.class);
200      }
201
202      @Override /* Overridden from Context.Builder<?> */
203      public Builder copy() {
204         return new Builder(this);
205      }
206
207   }
208
209   /**
210    * Default serializer, all default settings.
211    */
212   public static final MarkdownDocSerializer DEFAULT = new MarkdownDocSerializer(create());
213
214   /**
215    * Creates a new builder for this object.
216    *
217    * @return A new builder.
218    */
219   public static Builder create() {
220      return new Builder();
221   }
222
223   final String title;
224   final int headingLevel;
225   final boolean addHorizontalRules;
226   final String headerContent;
227   final String footerContent;
228
229   /**
230    * Constructor.
231    *
232    * @param builder The builder for this object.
233    */
234   public MarkdownDocSerializer(Builder builder) {
235      super(builder);
236      title = builder.title;
237      headingLevel = builder.headingLevel;
238      addHorizontalRules = builder.addHorizontalRules;
239      headerContent = builder.headerContent;
240      footerContent = builder.footerContent;
241   }
242
243   @Override /* Overridden from Context */
244   public Builder copy() {
245      return new Builder(this);
246   }
247
248   @Override /* Overridden from Context */
249   public MarkdownDocSerializerSession.Builder createSession() {
250      return MarkdownDocSerializerSession.create(this);
251   }
252
253   @Override /* Overridden from Context */
254   public MarkdownDocSerializerSession getSession() { return createSession().build(); }
255}