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}