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.hocon;
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.parser.*;
027import org.apache.juneau.marshall.stream.*;
028
029/**
030 * Parses HOCON (Human-Optimized Config Object Notation) text into POJO models.
031 *
032 * <h5 class='topic'>Media types</h5>
033 * <p>
034 * Handles <c>Content-Type</c> types:  <bc>application/hocon</bc>
035 *
036 * <h5 class='topic'>Description</h5>
037 * <p>
038 * Multi-pass parsing strategy: tokenize → build intermediate tree → resolve substitutions → convert to bean.
039 *
040 * <p>
041 * Accepts all three key-value separators: <c>=</c> (HOCON convention), <c>:</c> (JSON style), and whitespace before <c>{</c> (for object values).
042 *
043 * <p>
044 * Supports unquoted string keys and values, path expressions (<c>a.b.c = value</c>), object merging, substitution resolution,
045 * triple-quoted strings, both <c>//</c> and <c>#</c> comments, and optional root braces.
046 *
047 * <h5 class='figure'>Example input (bean):</h5>
048 * <p class='bjson'>
049 * name = myapp
050 * port = 8080
051 * debug = true
052 * database {
053 *   host = localhost
054 *   port = 5432
055 * }
056 * tags = [web, api, rest]
057 * </p>
058 *
059 * <h5 class='section'>Limitations:</h5><ul>
060 *    <li class='note'><c>include</c> directives are not supported — they are ignored with a warning during parsing.
061 *    <li class='note'>Duration/size unit suffixes (<c>10s</c>, <c>5m</c>, <c>512K</c>, <c>2G</c>) are treated as unquoted strings.
062 *    <li class='note'>Substitutions are resolved before bean conversion; the resolved (concrete) values are what Juneau sees.
063 * </ul>
064 *
065 * <h5 class='section'>See Also:</h5><ul>
066 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/HoconSupport">HOCON</a>
067 * </ul>
068 */
069@SuppressWarnings({
070   "java:S110", // Depth comes from the shared Parser -> ReaderParser base hierarchy
071   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
072   "java:S9149", // Per-format static factories intentionally shadow the parent's.
073   "resource" // Closeable resources are owned by the caller's parser session; Eclipse JDT @Owning warning is by design.
074})
075public class HoconParser extends ReaderParser implements HoconMetaProvider, RecordReadable {
076
077   private final java.util.concurrent.ConcurrentHashMap<ClassMeta<?>,HoconClassMeta> hoconClassMetas = new java.util.concurrent.ConcurrentHashMap<>();
078   private final java.util.concurrent.ConcurrentHashMap<BeanPropertyMeta,HoconBeanPropertyMeta> hoconBeanPropertyMetas = new java.util.concurrent.ConcurrentHashMap<>();
079
080   /** Whether to resolve ${var} substitutions. */
081   protected final boolean resolveSubstitutions;
082
083   /**
084    * Builder class.
085    */
086   public static class Builder extends ReaderParser.Builder<Builder> {
087
088      private static final Cache<HashKey,HoconParser> CACHE = Cache.of(HashKey.class, HoconParser.class).build();
089
090      private boolean resolveSubstitutions = true;
091
092      protected Builder() {
093         consumes("application/hocon");
094      }
095
096      protected Builder(Builder copyFrom) {
097         super(reqnn("ctx", copyFrom));
098         resolveSubstitutions = copyFrom.resolveSubstitutions;
099      }
100
101      protected Builder(HoconParser copyFrom) {
102         super(reqnn("ctx", copyFrom));
103         resolveSubstitutions = copyFrom.resolveSubstitutions;
104      }
105
106      /**
107       * Enables substitution resolution.
108       *
109       * @param value <jk>true</jk> to resolve ${var} and ${?var}.
110       * @return This object.
111       */
112      public Builder resolveSubstitutions(boolean value) {
113         resolveSubstitutions = value;
114         return this;
115      }
116
117      @Override
118      public HoconParser build() {
119         return cache(CACHE).build(HoconParser.class);
120      }
121
122      @Override
123      public Builder copy() {
124         return new Builder(this);
125      }
126
127      @Override
128      public HashKey hashKey() {
129         return HashKey.of(super.hashKey(), resolveSubstitutions);
130      }
131   }
132
133   /** Default parser. */
134   public static final HoconParser DEFAULT = new HoconParser(create());
135
136   /**
137    * Creates a new builder.
138    *
139    * @return A new builder.
140    */
141   public static Builder create() {
142      return new Builder();
143   }
144
145   /**
146    * Constructor.
147    *
148    * @param builder The builder.
149    */
150   public HoconParser(Builder builder) {
151      super(builder);
152      resolveSubstitutions = builder.resolveSubstitutions;
153   }
154
155   @Override
156   public HoconParserSession.Builder createSession() {
157      return HoconParserSession.create(this);
158   }
159
160   @Override
161   public Builder copy() {
162      return new Builder(this);
163   }
164
165   @Override
166   public HoconBeanPropertyMeta getHoconBeanPropertyMeta(BeanPropertyMeta bpm) {
167      if (bpm == null)
168         return HoconBeanPropertyMeta.DEFAULT;
169      return hoconBeanPropertyMetas.computeIfAbsent(bpm, k -> new HoconBeanPropertyMeta(k, this));
170   }
171
172   @Override
173   public HoconClassMeta getHoconClassMeta(ClassMeta<?> cm) {
174      return hoconClassMetas.computeIfAbsent(cm, k -> new HoconClassMeta(k, this));
175   }
176
177   /**
178    * Convenience delegator that opens a {@link RecordReader} over the input using
179    * <b>default session arguments</b> (mirrors {@link #read(Object, Class)}).
180    *
181    * <p>
182    * The real implementation lives on {@link HoconParserSession#readRecords(Object)}.  Callers
183    * that need request-derived configuration (locale, timezone, schema, swaps) should call
184    * {@link #createSession()} and invoke {@link HoconParserSession#readRecords(Object)} on the
185    * built session instead.
186    *
187    * @param input The input.
188    * @return A new {@link RecordReader} cursor.
189    * @throws IOException If a problem occurred opening the underlying input.
190    */
191   @Override /* RecordReadable */
192   public RecordReader readRecords(Object input) throws IOException {
193      return ((RecordReadable) getSession()).readRecords(input);
194   }
195
196   @Override /* RecordReadable */
197   public boolean isRecordStreaming() {
198      return false;
199   }
200}