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.ini;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.io.*;
022import java.time.*;
023
024import org.apache.juneau.commons.bean.*;
025import org.apache.juneau.commons.collections.*;
026import org.apache.juneau.marshall.*;
027import org.apache.juneau.marshall.parser.*;
028import org.apache.juneau.marshall.stream.*;
029
030/**
031 * Parses INI-formatted text into POJO models.
032 *
033 * <p>
034 * Parses INI files (section/key-value format) into Java beans and maps.
035 *
036 * <h5 class='topic'>Media types</h5>
037 * <p>
038 * Consumes: <bc>text/ini, text/x-ini</bc>
039 *
040 * <h5 class='topic'>Value parsing</h5>
041 * <ul class='spaced-list'>
042 *    <li>Unquoted <c>null</c> → Java <jk>null</jk>
043 *    <li>Unquoted <c>true</c>/<c>false</c> → Boolean
044 *    <li>Unquoted numbers → Number (int, long, float, double as appropriate)
045 *    <li>Single-quoted strings (<c>'...'</c>) → String (with <c>''</c> unescaped to single quote)
046 *    <li>Values starting with <c>[</c> or <c>{</c> → Delegated to JSON parser
047 *    <li>ISO 8601 strings → Date, Calendar, or <c>java.time.*</c> when target requires it
048 *    <li>ISO 8601 duration strings → {@link Duration}
049 *    <li>Other unquoted tokens → String
050 * </ul>
051 *
052 * <h5 class='topic'>Sections</h5>
053 * <p>
054 * Sections (e.g. <c>[address]</c>) map to nested bean properties or Map properties.
055 * Section paths like <c>[employment/company]</c> map to deeply nested bean properties.
056 *
057 * <h5 class='section'>Example:</h5>
058 * <p class='bjava'>
059 *    <jc>// Parse INI into a bean</jc>
060 *    MyConfig <jv>config</jv> = IniParser.<jsf>DEFAULT</jsf>.read(<jv>ini</jv>, MyConfig.<jk>class</jk>);
061 *
062 *    <jc>// Parse into a Map</jc>
063 *    Map&lt;String, Object&gt; <jv>map</jv> = IniParser.<jsf>DEFAULT</jsf>.read(<jv>ini</jv>, Map.<jk>class</jk>, String.<jk>class</jk>, Object.<jk>class</jk>);
064 * </p>
065 *
066 * <h5 class='figure'>Example input:</h5>
067 * <p class='bini'>
068 *    <ck>name</ck> = <cv>Alice</cv>
069 *    <ck>age</ck> = <cv>30</cv>
070 *
071 *    <cs>[address]</cs>
072 *    <ck>street</ck> = <cv>123 Main St</cv>
073 *    <ck>city</ck> = <cv>Boston</cv>
074 *    <ck>state</ck> = <cv>MA</cv>
075 * </p>
076 *
077 * <h5 class='topic'>Limitations</h5>
078 * <p>
079 * Parsing into top-level collections, arrays, or scalar types is not supported.
080 * The target type must be a bean class or <c>Map&lt;String,?&gt;</c>.
081 *
082 * <h5 class='section'>Notes:</h5><ul>
083 *    <li class='note'>This class is thread safe and reusable.
084 *    <li class='note'>Values starting with <c>[</c> or <c>{</c> are delegated to the JSON parser.
085 * </ul>
086 *
087 * <h5 class='section'>See Also:</h5><ul>
088 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Ini">INI Basics</a>
089 * </ul>
090 */
091@SuppressWarnings({
092   "java:S110", // IniParser sits below Parser and ReaderParser in the parser hierarchy, which is structural
093   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
094   "java:S9149", // Per-format static factories intentionally shadow the parent's.
095   "resource" // Closeable resources are owned by the caller's parser session; Eclipse JDT @Owning warning is by design.
096})
097public class IniParser extends ReaderParser implements IniMetaProvider, RecordReadable {
098
099   private final java.util.concurrent.ConcurrentHashMap<ClassMeta<?>,IniClassMeta> iniClassMetas = new java.util.concurrent.ConcurrentHashMap<>();
100   private final java.util.concurrent.ConcurrentHashMap<BeanPropertyMeta,IniBeanPropertyMeta> iniBeanPropertyMetas = new java.util.concurrent.ConcurrentHashMap<>();
101
102   /**
103    * Builder for {@link IniParser}.
104    */
105   public static class Builder extends ReaderParser.Builder<Builder> {
106
107      private static final Cache<HashKey,IniParser> CACHE = Cache.of(HashKey.class, IniParser.class).build();
108
109      private char kvSeparator = '=';
110
111      protected Builder() {
112         consumes("text/ini,text/x-ini");
113      }
114
115      protected Builder(Builder copyFrom) {
116         super(reqnn("copyFrom", copyFrom));
117         kvSeparator = copyFrom.kvSeparator;
118      }
119
120      protected Builder(IniParser copyFrom) {
121         super(reqnn("copyFrom", copyFrom));
122         kvSeparator = copyFrom.kvSeparator;
123      }
124
125      /**
126       * Key-value separator character.
127       *
128       * <p>
129       * By default, the parser recognizes both <c>=</c> and <c>:</c> as key-value separators (matching
130       * {@link IniSerializer.Builder#kvSeparator(char)}'s default of <c>=</c>). Set this to match a
131       * serializer configured with a non-default {@code kvSeparator} so the resulting INI can be
132       * round-tripped.
133       *
134       * @param value The key-value separator character to recognize (in addition to the default <c>=</c>/<c>:</c> pair).
135       * @return This object.
136       */
137      public Builder kvSeparator(char value) {
138         kvSeparator = value;
139         return this;
140      }
141
142      @Override
143      public IniParser build() {
144         return cache(CACHE).build(IniParser.class);
145      }
146
147      @Override
148      public Builder copy() {
149         return new Builder(this);
150      }
151
152      @Override /* Overridden from Context.Builder<?> */
153      public HashKey hashKey() {
154         return HashKey.of(super.hashKey(), kvSeparator);
155      }
156   }
157
158   /** Default parser instance. */
159   public static final IniParser DEFAULT = new IniParser(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   /** Key-value separator. */
171   protected final char kvSeparator;
172
173   /**
174    * Constructor.
175    *
176    * @param builder The builder.
177    */
178   public IniParser(Builder builder) {
179      super(builder);
180      kvSeparator = builder.kvSeparator;
181   }
182
183   @Override
184   public IniBeanPropertyMeta getIniBeanPropertyMeta(BeanPropertyMeta bpm) {
185      if (bpm == null)
186         return IniBeanPropertyMeta.DEFAULT;
187      return iniBeanPropertyMetas.computeIfAbsent(bpm, k -> new IniBeanPropertyMeta(k, this));
188   }
189
190   @Override
191   public IniClassMeta getIniClassMeta(ClassMeta<?> cm) {
192      return iniClassMetas.computeIfAbsent(cm, k -> new IniClassMeta(k, this));
193   }
194
195   @Override
196   public IniParserSession.Builder createSession() {
197      return IniParserSession.create(this);
198   }
199
200   @Override
201   public Builder copy() {
202      return new Builder(this);
203   }
204
205   @Override
206   protected FluentMap<String,Object> properties() {
207      return super.properties()
208         .a("kvSeparator", String.valueOf(kvSeparator));
209   }
210
211   /**
212    * Convenience delegator that opens a {@link RecordReader} over the input using
213    * <b>default session arguments</b> (mirrors {@link #read(Object, Class)}).
214    *
215    * <p>
216    * The real implementation lives on {@link IniParserSession#readRecords(Object)}.  Callers
217    * that need request-derived configuration (locale, timezone, schema, swaps) should call
218    * {@link #createSession()} and invoke {@link IniParserSession#readRecords(Object)} on the
219    * built session instead.
220    *
221    * @param input The input.
222    * @return A new {@link RecordReader} cursor.
223    * @throws IOException If a problem occurred opening the underlying input.
224    */
225   @Override /* RecordReadable */
226   public RecordReader readRecords(Object input) throws IOException {
227      return ((RecordReadable) getSession()).readRecords(input);
228   }
229
230   @Override /* RecordReadable */
231   public boolean isRecordStreaming() {
232      return false;
233   }
234}