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.hjson;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.io.*;
022
023import org.apache.juneau.commons.bean.*;
024import org.apache.juneau.marshall.*;
025import org.apache.juneau.marshall.collections.*;
026import org.apache.juneau.marshall.parser.*;
027import org.apache.juneau.marshall.stream.*;
028
029/**
030 * Parses Hjson (Human JSON) text into POJO models.
031 *
032 * <h5 class='topic'>Media types</h5>
033 * <p>
034 * Handles <c>Content-Type</c> types:  <bc>application/hjson, application/hjson+json</bc>
035 *
036 * <h5 class='topic'>Description</h5>
037 * <p>
038 * This parser handles all standard Hjson syntax as defined by the
039 * <a class="doclink" href="https://hjson.github.io/syntax.html">Hjson specification</a>.
040 *
041 * <p>
042 * In addition to standard JSON, this parser handles the following:
043 * <ul class='spaced-list'>
044 *    <li>
045 *       {@code #} single-line comments (to end of line).
046 *    <li>
047 *       {@code //} single-line comments.
048 *    <li>
049 *       {@code /* ... *}{@code /} block comments.
050 *    <li>
051 *       Quoteless string values (text read to end of line, leading/trailing whitespace trimmed).
052 *    <li>
053 *       Multiline strings delimited by {@code '''} (triple single-quote) with indentation stripping.
054 *    <li>
055 *       Optional commas — newlines may serve as member/element separators.
056 *    <li>
057 *       Trailing commas (ignored).
058 *    <li>
059 *       Unquoted keys.
060 *    <li>
061 *       Root-braceless objects (top-level content without wrapping {@code {}}).
062 * </ul>
063 *
064 * <p>
065 * Value type disambiguation: the parser identifies value types in this order:
066 * number → boolean → null → quoteless string.
067 * A value such as {@code 5 times} that starts like a number but contains non-numeric
068 * characters is treated as a quoteless string.
069 *
070 * <p>
071 * This parser handles the following input and returns the corresponding Java type:
072 * <ul class='spaced-list'>
073 *    <li>
074 *       Hjson/JSON objects ({@code {...}}) are converted to {@link JsonMap JsonMaps}.
075 *       <b>Note:</b> If a <code><xa>_type</xa>=<xs>'xxx'</xs></code> attribute is specified on the object,
076 *       an attempt is made to convert the object to an instance of the specified Java bean class.
077 *    <li>
078 *       Hjson/JSON arrays ({@code [...]}) are converted to {@link JsonList JsonLists}.
079 *    <li>
080 *       Quoted strings ({@code "..."} or {@code '...'}) are converted to {@link String Strings}.
081 *    <li>
082 *       Multiline strings ({@code '''...'''}) are converted to {@link String Strings}.
083 *    <li>
084 *       Quoteless strings are converted to {@link String Strings}.
085 *    <li>
086 *       Numbers are converted to {@link Integer Integers}, {@link Long Longs},
087 *       {@link Float Floats}, or {@link Double Doubles} depending on size and format.
088 *    <li>
089 *       {@code true}/{@code false} are converted to {@link Boolean Booleans}.
090 *    <li>
091 *       {@code null} returns <jk>null</jk>.
092 *    <li>
093 *       Input consisting of only whitespace or comments returns <jk>null</jk>.
094 * </ul>
095 *
096 * <h5 class='figure'>Example input - readable mode:</h5>
097 * <p class='bjson'>
098 *    {
099 *      name: Alice
100 *      age: 30
101 *    }
102 * </p>
103 *
104 * <h5 class='figure'>Example input - compact mode:</h5>
105 * <p class='bjson'>
106 *    {name:Alice,age:30}
107 * </p>
108 *
109 * <h5 class='section'>Notes:</h5><ul>
110 *    <li class='note'>Root-braceless Hjson files (content without a wrapping {@code {}}) are supported on input,
111 *       but {@link HjsonSerializer} always emits root braces by default.
112 *       A parse-then-serialize round-trip of a root-braceless file will produce braced output.
113 *    <li class='note'>This class is thread safe and reusable.
114 * </ul>
115 *
116 * <h5 class='section'>See Also:</h5><ul>
117 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Hjson">Hjson</a>
118 *    <li class='link'><a class="doclink" href="https://hjson.github.io/syntax.html">Hjson Specification</a>
119 * </ul>
120 */
121@SuppressWarnings({
122   "java:S110", // Extends ReaderParser, which sits in the shared Context > Parser > ReaderParser hierarchy used by every character-based parser
123   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
124   "java:S9149", // Per-format static factories intentionally shadow the parent's.
125   "resource" // Closeable resources are owned by the caller's parser session; Eclipse JDT @Owning warning is by design.
126})
127public class HjsonParser extends ReaderParser implements HjsonMetaProvider, RecordReadable {
128
129   private final java.util.concurrent.ConcurrentHashMap<ClassMeta<?>,HjsonClassMeta> hjsonClassMetas = new java.util.concurrent.ConcurrentHashMap<>();
130   private final java.util.concurrent.ConcurrentHashMap<BeanPropertyMeta,HjsonBeanPropertyMeta> hjsonBeanPropertyMetas = new java.util.concurrent.ConcurrentHashMap<>();
131
132   /**
133    * Builder class.
134    */
135   public static class Builder extends ReaderParser.Builder<Builder> {
136
137      protected Builder() {
138         consumes("application/hjson,application/hjson+json");
139      }
140
141      protected Builder(Builder copyFrom) {
142         super(reqnn("ctx", copyFrom));
143      }
144
145      protected Builder(HjsonParser copyFrom) {
146         super(reqnn("ctx", copyFrom));
147      }
148
149      @Override
150      public HjsonParser build() {
151         return new HjsonParser(this);
152      }
153
154      @Override
155      public Builder copy() {
156         return new Builder(this);
157      }
158
159   }
160
161   /** Default parser. */
162   public static final HjsonParser DEFAULT = new HjsonParser(create());
163
164   /**
165    * Creates a new builder.
166    *
167    * @return A new builder.
168    */
169   public static Builder create() {
170      return new Builder();
171   }
172
173   /**
174    * Constructor.
175    *
176    * @param builder The builder.
177    */
178   public HjsonParser(Builder builder) {
179      super(builder);
180   }
181
182   @Override
183   public HjsonParserSession.Builder createSession() {
184      return HjsonParserSession.create(this);
185   }
186
187   @Override
188   public Builder copy() {
189      return new Builder(this);
190   }
191
192   @Override
193   public HjsonBeanPropertyMeta getHjsonBeanPropertyMeta(BeanPropertyMeta bpm) {
194      if (bpm == null)
195         return HjsonBeanPropertyMeta.DEFAULT;
196      return hjsonBeanPropertyMetas.computeIfAbsent(bpm, k -> new HjsonBeanPropertyMeta(k, this));
197   }
198
199   @Override
200   public HjsonClassMeta getHjsonClassMeta(ClassMeta<?> cm) {
201      return hjsonClassMetas.computeIfAbsent(cm, k -> new HjsonClassMeta(k, this));
202   }
203
204   /**
205    * Convenience delegator that opens a {@link RecordReader} over the input using
206    * <b>default session arguments</b> (mirrors {@link #read(Object, Class)}).
207    *
208    * <p>
209    * The real implementation lives on {@link HjsonParserSession#readRecords(Object)}.  Callers
210    * that need request-derived configuration (locale, timezone, schema, swaps) should call
211    * {@link #createSession()} and invoke {@link HjsonParserSession#readRecords(Object)} on the
212    * built session instead.
213    *
214    * @param input The input.
215    * @return A new {@link RecordReader} cursor.
216    * @throws IOException If a problem occurred opening the underlying input.
217    */
218   @Override /* RecordReadable */
219   public RecordReader readRecords(Object input) throws IOException {
220      return ((RecordReadable) getSession()).readRecords(input);
221   }
222
223   @Override /* RecordReadable */
224   public boolean isRecordStreaming() {
225      return false;
226   }
227}