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.json5.*; 029import org.apache.juneau.marshall.parser.*; 030import org.apache.juneau.marshall.stream.*; 031 032/** 033 * Parses Markdown fragment-mode output (tables and lists) back to POJOs. 034 * 035 * <h5 class='topic'>Media types</h5> 036 * <p> 037 * Handles <c>Content-Type</c> types: <bc>text/markdown, text/x-markdown</bc> 038 * 039 * <h5 class='topic'>Description</h5> 040 * <p> 041 * Parses Markdown tables and bulleted lists produced by {@link MarkdownSerializer} back into Java objects. 042 * The following mappings apply: 043 * <ul class='spaced-list'> 044 * <li>2-column key/value tables are parsed as beans or {@link Map}s. 045 * <li>Multi-column tables are parsed as {@link List}s of beans or {@link Map}s. 046 * <li>Bulleted lists are parsed as {@link List}s. 047 * <li>Table cells containing backtick-wrapped JSON5 values are parsed via {@link Json5Parser}. 048 * </ul> 049 * 050 * <h5 class='section'>Example:</h5> 051 * <p class='bjava'> 052 * <jc>// Parse a Markdown table to a bean</jc> 053 * String <jv>md</jv> = <js>"| Property | Value |\n|---|---|\n| name | Alice |\n| age | 30 |"</js>; 054 * Person <jv>p</jv> = MarkdownParser.<jsf>DEFAULT</jsf>.read(<jv>md</jv>, Person.<jk>class</jk>); 055 * </p> 056 * <p class='bjava'> 057 * <jc>// Round-trip example</jc> 058 * String <jv>md</jv> = MarkdownSerializer.<jsf>DEFAULT</jsf>.write(<jv>myBean</jv>); 059 * MyBean <jv>parsed</jv> = MarkdownParser.<jsf>DEFAULT</jsf>.read(<jv>md</jv>, MyBean.<jk>class</jk>); 060 * </p> 061 * 062 * <h5 class='section'>Limitations:</h5><ul> 063 * <li class='note'>Multi-line string values cannot round-trip; newlines in cells are literal {@code <br>} text. 064 * <li class='note'>The configured null value string (default: {@code *null*}) is interpreted as {@code null}, not as the literal string. 065 * <li class='note'>Without a target type, table cell values are auto-detected as Integer, Long, Double, Boolean, or String. 066 * <li class='note'>Type ambiguity: without a target type, tables return {@code Map<String,Object>} (key/value) or {@code List<Map>} (multi-column). 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/Markdown">Markdown</a> 072 * </ul> 073 */ 074@SuppressWarnings({ 075 "java:S110", // Inheritance depth acceptable for this class hierarchy 076 "java:S115", // Constants use UPPER_snakeCase naming convention 077 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 078 "java:S9149", // Per-format static factories intentionally shadow the parent's. 079 "resource" // Closeable resources are owned by the caller's parser session; Eclipse JDT @Owning warning is by design. 080}) 081public class MarkdownParser extends ReaderParser implements MarkdownMetaProvider, RecordReadable { 082 083 /** 084 * Builder class. 085 */ 086 @SuppressWarnings({ 087 "java:S119" // 'SELF' (CRTP self-type) is intentional and clearer than a single-letter name. 088 }) 089 public abstract static class Builder<SELF extends Builder<SELF>> extends ReaderParser.Builder<SELF> { 090 091 private static final Cache<HashKey,MarkdownParser> CACHE = Cache.of(HashKey.class, MarkdownParser.class).build(); 092 093 String nullValue; 094 095 /** 096 * Constructor, default settings. 097 */ 098 protected Builder() { 099 consumes("text/markdown,text/x-markdown"); 100 nullValue = "*null*"; 101 } 102 103 /** 104 * Copy constructor. 105 * 106 * @param copyFrom The builder to copy from. 107 * <br>Cannot be <jk>null</jk>. 108 */ 109 protected Builder(Builder<?> copyFrom) { 110 super(reqnn("copyFrom", copyFrom)); 111 nullValue = copyFrom.nullValue; 112 } 113 114 /** 115 * Copy constructor. 116 * 117 * @param copyFrom The bean to copy from. 118 * <br>Cannot be <jk>null</jk>. 119 */ 120 protected Builder(MarkdownParser copyFrom) { 121 super(reqnn("copyFrom", copyFrom)); 122 nullValue = copyFrom.nullValue; 123 } 124 125 /** 126 * The string that is interpreted as {@code null} when parsing cell values. 127 * 128 * <p> 129 * Must match the value configured on the serializer. Default is {@code *null*}. 130 * 131 * @param value The null marker string. 132 * @return This object. 133 */ 134 public SELF nullValue(String value) { 135 nullValue = value == null ? "*null*" : value; 136 return self(); 137 } 138 139 @Override /* Overridden from Context.Builder<?> */ 140 public HashKey hashKey() { 141 return HashKey.of(super.hashKey(), nullValue); 142 } 143 144 @Override /* Overridden from Context.Builder<?> */ 145 public MarkdownParser build() { 146 return cache(CACHE).build(MarkdownParser.class); 147 } 148 149 @Override /* Overridden from Context.Builder<?> */ 150 public abstract SELF copy(); 151 152 153 } 154 155 /** 156 * Concrete default builder leaf for the non-subclassed {@link MarkdownParser#create()} / {@link MarkdownParser#copy()} path. 157 */ 158 public static final class DefaultBuilder extends Builder<DefaultBuilder> { 159 160 DefaultBuilder() {} 161 162 DefaultBuilder(MarkdownParser copyFrom) { 163 super(copyFrom); 164 } 165 166 DefaultBuilder(Builder<?> copyFrom) { 167 super(copyFrom); 168 } 169 170 @Override /* Overridden from Context.Builder<?> */ 171 public DefaultBuilder copy() { 172 return new DefaultBuilder(this); 173 } 174 } 175 176 /** 177 * Default parser, all default settings. 178 */ 179 public static final MarkdownParser DEFAULT = new MarkdownParser(create()); 180 181 /** 182 * Creates a new builder for this object. 183 * 184 * @return A new builder. 185 */ 186 @SuppressWarnings({ 187 "java:S1452" // Builder<?> wildcard return intentional; callers chain via fluent API without needing the concrete type 188 }) 189 public static Builder<?> create() { 190 return new DefaultBuilder(); 191 } 192 193 private final Map<ClassMeta<?>,MarkdownClassMeta> markdownClassMetas = new ConcurrentHashMap<>(); 194 private final Map<BeanPropertyMeta,MarkdownBeanPropertyMeta> markdownBeanPropertyMetas = new ConcurrentHashMap<>(); 195 final String nullValue; 196 197 /** 198 * Constructor. 199 * 200 * @param builder The builder for this object. 201 */ 202 public MarkdownParser(Builder<?> builder) { 203 super(builder); 204 nullValue = or(builder.nullValue, "*null*"); 205 } 206 207 /** 208 * Returns the string that is interpreted as null when parsing. 209 * 210 * @return The null marker. 211 */ 212 public String getNullValue() { 213 return nullValue; 214 } 215 216 @Override /* Overridden from Context */ 217 public Builder<?> copy() { 218 return new DefaultBuilder(this); 219 } 220 221 @Override /* Overridden from Context */ 222 public MarkdownParserSession.Builder<?> createSession() { 223 return MarkdownParserSession.create(this); 224 } 225 226 @Override /* Overridden from MarkdownMetaProvider */ 227 public MarkdownBeanPropertyMeta getMarkdownBeanPropertyMeta(BeanPropertyMeta bpm) { 228 if (bpm == null) 229 return MarkdownBeanPropertyMeta.DEFAULT; 230 return markdownBeanPropertyMetas.computeIfAbsent(bpm, k -> new MarkdownBeanPropertyMeta(k.getDelegateFor(), this)); 231 } 232 233 @Override /* Overridden from MarkdownMetaProvider */ 234 public MarkdownClassMeta getMarkdownClassMeta(ClassMeta<?> cm) { 235 return markdownClassMetas.computeIfAbsent(cm, k -> new MarkdownClassMeta(k, this)); 236 } 237 238 @Override /* Overridden from Context */ 239 public MarkdownParserSession getSession() { return createSession().build(); } 240 241 /** 242 * Convenience delegator that opens a {@link RecordReader} over the input using 243 * <b>default session arguments</b> (mirrors {@link #read(Object, Class)}). 244 * 245 * <p> 246 * The real implementation lives on {@link MarkdownParserSession#readRecords(Object)}. Callers 247 * that need request-derived configuration (locale, timezone, schema, swaps) should call 248 * {@link #createSession()} and invoke {@link MarkdownParserSession#readRecords(Object)} on the 249 * built session instead. 250 * 251 * @param input The input. 252 * @return A new {@link RecordReader} cursor. 253 * @throws IOException If a problem occurred opening the underlying input. 254 */ 255 @Override /* RecordReadable */ 256 public RecordReader readRecords(Object input) throws IOException { 257 return getSession().readRecords(input); 258 } 259 260 @Override /* RecordReadable */ 261 public boolean isRecordStreaming() { 262 return false; 263 } 264}