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.*; 022 023import org.apache.juneau.commons.bean.*; 024import org.apache.juneau.commons.collections.*; 025import org.apache.juneau.marshall.*; 026import org.apache.juneau.marshall.serializer.*; 027import org.apache.juneau.marshall.stream.*; 028 029/** 030 * Serializes POJO models to INI format. 031 * 032 * <p> 033 * Converts Java POJOs to INI format (section/key-value structure). 034 * 035 * <h5 class='topic'>Media types</h5> 036 * <p> 037 * Produces: <bc>text/ini</bc> 038 * <br>Accepts: <bc>text/ini, text/x-ini</bc> 039 * 040 * <h5 class='topic'>Bean-to-INI mapping</h5> 041 * <ul class='spaced-list'> 042 * <li>Simple properties → key-value pairs in the default section 043 * <li>Nested beans → <c>[sectionName]</c> sections 044 * <li>Deeply nested beans → <c>[path/to/section]</c> section paths 045 * <li>Collections and complex values → JSON5-encoded inline values 046 * <li>Maps with string keys → <c>[sectionName]</c> sections 047 * <li>Null values → unquoted <c>null</c> token 048 * </ul> 049 * 050 * <h5 class='section'>Example:</h5> 051 * <p class='bjava'> 052 * <jc>// Serialize a bean to INI</jc> 053 * String <jv>ini</jv> = IniSerializer.<jsf>DEFAULT</jsf>.write(<jv>myBean</jv>); 054 * 055 * <jc>// Create a custom serializer</jc> 056 * IniSerializer <jv>s</jv> = IniSerializer.<jsm>create</jsm>().useComments().build(); 057 * <jv>ini</jv> = <jv>s</jv>.write(<jv>myBean</jv>); 058 * </p> 059 * 060 * <h5 class='figure'>Example output (Map of name/age):</h5> 061 * <p class='bini'> 062 * <ck>name</ck> = <cv>Alice</cv> 063 * <ck>age</ck> = <cv>30</cv> 064 * </p> 065 * 066 * <h5 class='figure'>Complex (nested object + array):</h5> 067 * <p class='bini'> 068 * <ck>name</ck> = <cv>Alice</cv> 069 * <ck>age</ck> = <cv>30</cv> 070 * <ck>tags</ck> = <cv>['a','b','c']</cv> 071 * 072 * <cs>[address]</cs> 073 * <ck>street</ck> = <cv>123 Main St</cv> 074 * <ck>city</ck> = <cv>Boston</cv> 075 * <ck>state</ck> = <cv>MA</cv> 076 * </p> 077 * 078 * <h5 class='topic'>Limitations</h5> 079 * <p> 080 * <ul class='spaced-list'> 081 * <li>Top-level collections, arrays, and scalar values are not supported. The root must be a bean or 082 * <c>Map<String,?></c>. Throws {@link SerializeException} for unsupported root types. 083 * <li>{@link Reader} and {@link InputStream} passthrough is not supported. 084 * </ul> 085 * 086 * <h5 class='section'>Notes:</h5><ul> 087 * <li class='note'>This class is thread safe and reusable. 088 * <li class='note'>Complex values (collections, maps with complex values) are embedded as JSON5 inline 089 * strings and remain fully round-trippable. 090 * </ul> 091 * 092 * <h5 class='section'>See Also:</h5><ul> 093 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Ini">INI Basics</a> 094 * </ul> 095 */ 096@SuppressWarnings({ 097 "java:S110", // Serializer hierarchy (IniSerializer -> WriterSerializer -> Serializer -> MarshallingContextable -> Context) is inherently deep. 098 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 099 "java:S9149", // Per-format static factories intentionally shadow the parent's. 100 "resource" // Closeable resources are owned by the caller's serializer session; Eclipse JDT @Owning warning is by design. 101}) 102public class IniSerializer extends WriterSerializer implements IniMetaProvider, RecordWritable { 103 104 private final java.util.concurrent.ConcurrentHashMap<ClassMeta<?>,IniClassMeta> iniClassMetas = new java.util.concurrent.ConcurrentHashMap<>(); 105 private final java.util.concurrent.ConcurrentHashMap<BeanPropertyMeta,IniBeanPropertyMeta> iniBeanPropertyMetas = new java.util.concurrent.ConcurrentHashMap<>(); 106 107 /** 108 * Builder class. 109 */ 110 public static class Builder extends WriterSerializer.Builder<Builder> { 111 112 private static final Cache<HashKey,IniSerializer> CACHE = Cache.of(HashKey.class, IniSerializer.class).build(); 113 114 private char kvSeparator = '='; 115 private boolean spacedSeparator = true; 116 private boolean useComments = false; 117 118 protected Builder() { 119 produces("text/ini"); 120 accept("text/ini,text/x-ini"); 121 } 122 123 protected Builder(Builder copyFrom) { 124 super(reqnn("copyFrom", copyFrom)); 125 kvSeparator = copyFrom.kvSeparator; 126 spacedSeparator = copyFrom.spacedSeparator; 127 useComments = copyFrom.useComments; 128 } 129 130 protected Builder(IniSerializer copyFrom) { 131 super(reqnn("copyFrom", copyFrom)); 132 kvSeparator = copyFrom.kvSeparator; 133 spacedSeparator = copyFrom.spacedSeparator; 134 useComments = copyFrom.useComments; 135 } 136 137 /** 138 * Key-value separator character. 139 * 140 * @param value <c>=</c> (default) or <c>:</c>. 141 * @return This object. 142 */ 143 public Builder kvSeparator(char value) { 144 kvSeparator = value; 145 return this; 146 } 147 148 /** 149 * Whether to add spaces around the separator. 150 * 151 * @param value <jk>true</jk> for <c>key = value</c>, <jk>false</jk> for <c>key=value</c>. 152 * @return This object. 153 */ 154 public Builder spacedSeparator(boolean value) { 155 spacedSeparator = value; 156 return this; 157 } 158 159 /** 160 * Whether to emit <c>#</c> comments from bean property descriptions. 161 * 162 * @param value The flag. 163 * @return This object. 164 */ 165 public Builder useComments(boolean value) { 166 useComments = value; 167 return this; 168 } 169 170 /** 171 * Emit <c>#</c> comments from bean property descriptions. 172 * 173 * @return This object. 174 */ 175 public Builder useComments() { 176 useComments = true; 177 return this; 178 } 179 180 @Override 181 public Builder useWhitespace() { 182 super.useWhitespace(); 183 return this; 184 } 185 186 @Override 187 public Builder useWhitespace(boolean value) { 188 super.useWhitespace(value); 189 return this; 190 } 191 192 @Override 193 public Builder ws() { 194 return useWhitespace(); 195 } 196 197 @Override 198 public IniSerializer build() { 199 return cache(CACHE).build(IniSerializer.class); 200 } 201 202 @Override 203 public Builder copy() { 204 return new Builder(this); 205 } 206 207 @Override 208 public HashKey hashKey() { 209 return HashKey.of(super.hashKey(), kvSeparator, spacedSeparator, useComments); 210 } 211 } 212 213 /** Default serializer. */ 214 public static final IniSerializer DEFAULT = new IniSerializer(create()); 215 216 /** Default serializer with blank lines between sections. */ 217 public static final IniSerializer DEFAULT_READABLE = new IniSerializer(create().ws()); 218 219 /** 220 * Creates a new builder. 221 * 222 * @return A new builder. 223 */ 224 public static Builder create() { 225 return new Builder(); 226 } 227 228 /** Key-value separator. */ 229 protected final char kvSeparator; 230 231 /** Whether to add spaces around the separator. */ 232 protected final boolean spacedSeparator; 233 234 /** Whether to emit property descriptions as comments. */ 235 protected final boolean useComments; 236 237 /** 238 * Constructor. 239 * 240 * @param builder The builder. 241 */ 242 public IniSerializer(Builder builder) { 243 super(builder); 244 kvSeparator = builder.kvSeparator; 245 spacedSeparator = builder.spacedSeparator; 246 useComments = builder.useComments; 247 } 248 249 @Override 250 public IniSerializerSession.Builder createSession() { 251 return IniSerializerSession.create(this); 252 } 253 254 @Override 255 public Builder copy() { 256 return new Builder(this); 257 } 258 259 @Override 260 public IniBeanPropertyMeta getIniBeanPropertyMeta(BeanPropertyMeta bpm) { 261 if (bpm == null) 262 return IniBeanPropertyMeta.DEFAULT; 263 return iniBeanPropertyMetas.computeIfAbsent(bpm, k -> new IniBeanPropertyMeta(k, this)); 264 } 265 266 @Override 267 public IniClassMeta getIniClassMeta(ClassMeta<?> cm) { 268 return iniClassMetas.computeIfAbsent(cm, k -> new IniClassMeta(k, this)); 269 } 270 271 @Override 272 protected FluentMap<String,Object> properties() { 273 return super.properties() 274 .a("kvSeparator", String.valueOf(kvSeparator)) 275 .a("spacedSeparator", spacedSeparator) 276 .a("useComments", useComments); 277 } 278 279 /** 280 * Convenience delegator that opens a {@link RecordWriter} over the output using 281 * <b>default session arguments</b> (mirrors {@link #write(Object)}). 282 * 283 * <p> 284 * The real implementation lives on {@link IniSerializerSession#writeRecords(Object)}. 285 * Callers that need request-derived configuration should call {@link #createSession()} and 286 * invoke {@link IniSerializerSession#writeRecords(Object)} on the built session instead. 287 * 288 * @param output The output. 289 * @return A new {@link RecordWriter}. 290 * @throws IOException If a problem occurred opening the underlying output. 291 */ 292 @Override /* RecordWritable */ 293 public RecordWriter writeRecords(Object output) throws IOException { 294 return ((RecordWritable) getSession()).writeRecords(output); 295 } 296 297 @Override /* RecordWritable */ 298 public boolean isRecordStreaming() { 299 return false; 300 } 301}