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<String, Object> <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<String,?></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}