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}