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