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.toml;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.io.*;
022
023import org.apache.juneau.commons.bean.*;
024import org.apache.juneau.commons.collections.*;
025import org.apache.juneau.marshall.*;
026import org.apache.juneau.marshall.json.*;
027import org.apache.juneau.marshall.parser.*;
028import org.apache.juneau.marshall.stream.*;
029
030/**
031 * Parses TOML v1.0.0 text into POJOs.
032 *
033 * <p>
034 * Handles <c>Content-Type</c>: <bc>application/toml</bc>
035 *
036 * <h5 class='topic'>Limitations compared to JSON</h5>
037 * <p>
038 * TOML is a configuration format with a more restricted type system than JSON.
039 * The following features supported by {@link JsonParser}
040 * are not available or limited in TOML:
041 * </p>
042 * <ul class='spaced-list'>
043 *    <li><b>Null values</b> — TOML has no null type. Absent keys yield null for object properties.
044 *       The parser treats the configured <c>nullValue</c> string (e.g. <c>&lt;NULL&gt;</c>) as Java null
045 *       when it appears as a value.
046 *    <li><b>Non-string map keys</b> — TOML keys are always strings. Non-string key types
047 *       (e.g. <c>Map&lt;Integer,String&gt;</c>) are converted via {@link ParserSession#convertAttrToType(Object, String, org.apache.juneau.marshall.ClassMeta) convertAttrToType}.
048 *       Null keys are serialized as the string <c>null</c> and converted back to Java null during parsing.
049 *    <li><b>Polymorphic types</b> — Parsing to interfaces or abstract classes requires a
050 *       {@link org.apache.juneau.marshall.MarshallingContext.Builder#beanDictionary(Class[]) bean dictionary}
051 *       when no <c>_type</c> discriminator is present in the document.
052 *    <li><b>Duration</b> — TOML has no native duration type. Quoted ISO-8601 duration strings
053 *       (e.g. <c>"PT1H30M"</c>) are parsed to <c>Duration</c> via {@link org.apache.juneau.marshall.utils.Iso8601Utils}.
054 *    <li><b>Year and YearMonth</b> — Bare <c>Year</c> (4-digit integer) parses as <c>Long</c>, not <c>Year</c>.
055 *       Use quoted strings (e.g. <c>"2024"</c>) or <c>yyyy-MM</c> for <c>YearMonth</c>; swaps may be needed
056 *       for full compatibility.
057 * </ul>
058 *
059 * <h5 class='section'>Example:</h5>
060 * <p class='bjava'>
061 *    <jc>// Parse TOML into a JsonMap</jc>
062 *    String <jv>toml</jv> = <js>"name = \"myapp\"\nport = 8080"</js>;
063 *    JsonMap <jv>parsed</jv> = TomlParser.<jsf>DEFAULT</jsf>.read(<jv>toml</jv>, JsonMap.<jk>class</jk>);
064 *
065 *    <jc>// Parse into a bean</jc>
066 *    MyConfig <jv>config</jv> = TomlParser.<jsf>DEFAULT</jsf>.read(<jv>toml</jv>, MyConfig.<jk>class</jk>);
067 *
068 *    <jc>// Parse into a parameterized type (e.g. Map with type args)</jc>
069 *    Map&lt;String, Object&gt; <jv>map</jv> = TomlParser.<jsf>DEFAULT</jsf>.read(<jv>toml</jv>, Map.<jk>class</jk>, String.<jk>class</jk>, Object.<jk>class</jk>);
070 *
071 *    <jc>// Use the marshaller for convenience</jc>
072 *    <jv>parsed</jv> = Toml.<jsm>to</jsm>(<jv>toml</jv>, JsonMap.<jk>class</jk>);
073 * </p>
074 *
075 * <h5 class='figure'>Example input (Map of name/age):</h5>
076 * <p class='bcode'>
077 *    name = "Alice"
078 *    age = 30
079 * </p>
080 *
081 * <h5 class='figure'>Complex (nested table + array):</h5>
082 * <p class='bcode'>
083 *    name = "Alice"
084 *    age = 30
085 *    tags = ["a", "b", "c"]
086 *
087 *    [address]
088 *    street = "123 Main St"
089 *    city = "Boston"
090 *    state = "MA"
091 * </p>
092 *
093 * <h5 class='section'>See Also:</h5><ul>
094 *    <li class='link'><a href="https://toml.io/en/v1.0.0">TOML v1.0.0 Specification</a>
095 * </ul>
096 */
097@SuppressWarnings({
098   "java:S110", // Builder pattern requires many parameters
099   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
100   "java:S9149", // Per-format static factories intentionally shadow the parent's.
101   "resource" // Closeable resources are owned by the caller's parser session; Eclipse JDT @Owning warning is by design.
102})
103public class TomlParser extends ReaderParser implements TomlMetaProvider, RecordReadable {
104
105   private final java.util.concurrent.ConcurrentHashMap<ClassMeta<?>,TomlClassMeta> tomlClassMetas = new java.util.concurrent.ConcurrentHashMap<>();
106   private final java.util.concurrent.ConcurrentHashMap<BeanPropertyMeta,TomlBeanPropertyMeta> tomlBeanPropertyMetas = new java.util.concurrent.ConcurrentHashMap<>();
107
108   /**
109    * Builder for {@link TomlParser}.
110    */
111   public static class Builder extends ReaderParser.Builder<Builder> {
112
113      private static final Cache<HashKey,TomlParser> CACHE = Cache.of(HashKey.class, TomlParser.class).build();
114
115      private String nullValue = "<NULL>";
116
117      protected Builder() {
118         consumes("application/toml");
119      }
120
121      protected Builder(Builder copyFrom) {
122         super(reqnn("copyFrom", copyFrom));
123         nullValue = copyFrom.nullValue;
124      }
125
126      protected Builder(TomlParser copyFrom) {
127         super(reqnn("copyFrom", copyFrom));
128         nullValue = copyFrom.nullValue;
129      }
130
131      /**
132       * String that denotes null when parsing. Must match the serializer's nullValue.
133       *
134       * @param value The null marker string.
135       * @return This object.
136       */
137      public Builder nullValue(String value) {
138         nullValue = value;
139         return this;
140      }
141
142      @Override
143      public TomlParser build() {
144         return cache(CACHE).build(TomlParser.class);
145      }
146
147      @Override
148      public Builder copy() {
149         return new Builder(this);
150      }
151
152      @Override
153      public HashKey hashKey() {
154         return HashKey.of(super.hashKey(), nullValue);
155      }
156   }
157
158   /** Default parser instance. */
159   public static final TomlParser DEFAULT = new TomlParser(create());
160
161   /**
162    * Creates a new builder.
163    *
164    * @return A new builder.
165    */
166   public static Builder create() {
167      return new Builder();
168   }
169
170   protected final String nullValue;
171
172   /**
173    * Constructor.
174    *
175    * @param builder The builder.
176    */
177   public TomlParser(Builder builder) {
178      super(builder);
179      nullValue = or(builder.nullValue, "<NULL>");
180   }
181
182   /**
183    * Returns the string treated as null when parsing.
184    *
185    * @return The null marker.
186    */
187   public String getNullValue() {
188      return nullValue;
189   }
190
191   @Override
192   public TomlParserSession.Builder createSession() {
193      return TomlParserSession.create(this);
194   }
195
196   @Override /* Overridden from TomlMetaProvider */
197   public TomlBeanPropertyMeta getTomlBeanPropertyMeta(BeanPropertyMeta bpm) {
198      if (bpm == null)
199         return TomlBeanPropertyMeta.DEFAULT;
200      return tomlBeanPropertyMetas.computeIfAbsent(bpm, k -> new TomlBeanPropertyMeta(k, this));
201   }
202
203   @Override /* Overridden from TomlMetaProvider */
204   public TomlClassMeta getTomlClassMeta(ClassMeta<?> cm) {
205      return tomlClassMetas.computeIfAbsent(cm, k -> new TomlClassMeta(k, this));
206   }
207
208   @Override
209   public Builder copy() {
210      return new Builder(this);
211   }
212
213   @Override
214   protected FluentMap<String,Object> properties() {
215      return super.properties().a("nullValue", nullValue);
216   }
217
218   /**
219    * Convenience delegator that opens a {@link RecordReader} over the input using
220    * <b>default session arguments</b> (mirrors {@link #read(Object, Class)}).
221    *
222    * <p>
223    * The real implementation lives on {@link TomlParserSession#readRecords(Object)}.  Callers
224    * that need request-derived configuration (locale, timezone, schema, swaps) should call
225    * {@link #createSession()} and invoke {@link TomlParserSession#readRecords(Object)} on the
226    * built session instead.
227    *
228    * @param input The input.
229    * @return A new {@link RecordReader} cursor.
230    * @throws IOException If a problem occurred opening the underlying input.
231    */
232   @Override /* RecordReadable */
233   public RecordReader readRecords(Object input) throws IOException {
234      return ((RecordReadable) getSession()).readRecords(input);
235   }
236
237   @Override /* RecordReadable */
238   public boolean isRecordStreaming() {
239      return false;
240   }
241}