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}