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.*; 023import java.time.temporal.*; 024import java.util.*; 025import java.util.function.*; 026 027import org.apache.juneau.commons.bean.*; 028import org.apache.juneau.commons.utils.*; 029import org.apache.juneau.marshall.*; 030import org.apache.juneau.marshall.json.*; 031import org.apache.juneau.marshall.json5.*; 032import org.apache.juneau.marshall.serializer.*; 033import org.apache.juneau.marshall.stream.*; 034 035/** 036 * Session for serializing objects to INI format. 037 */ 038@SuppressWarnings({ 039 "java:S110", // Inheritance depth acceptable for serializer session hierarchy 040 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 041 "java:S3776", // Cognitive complexity acceptable for serialization logic 042 "java:S6541", // Acceptable for session implementation 043 "resource" // IniWriter lifecycle managed by SerializerPipe 044}) 045public class IniSerializerSession extends WriterSerializerSession implements RecordWritable { 046 047 private static final String SECTION_PATH_DELIMITER = "/"; 048 049 /** 050 * Builder for INI serializer session. 051 */ 052 public static class Builder extends WriterSerializerSession.Builder<Builder> { 053 054 private final IniSerializer ctx; 055 056 protected Builder(IniSerializer ctx) { 057 super(reqnn("ctx", ctx)); 058 this.ctx = ctx; 059 } 060 061 @Override 062 public IniSerializerSession build() { 063 return new IniSerializerSession(this); 064 } 065 } 066 067 /** 068 * Creates a session builder. 069 * 070 * @param ctx The serializer context. 071 * <br>Cannot be <jk>null</jk>. 072 * @return The builder. 073 */ 074 public static Builder create(IniSerializer ctx) { 075 return new Builder(reqnn("ctx", ctx)); 076 } 077 078 private final IniSerializer ctx; 079 080 protected IniSerializerSession(Builder builder) { 081 super(builder); 082 ctx = builder.ctx; 083 } 084 085 @Override /* RecordWritable */ 086 public RecordWriter writeRecords(Object output) throws IOException { 087 return RecordAdapter.writer(this, output); 088 } 089 090 @Override /* RecordWritable */ 091 public boolean isRecordStreaming() { 092 return false; 093 } 094 095 @Override 096 protected void doWrite(SerializerPipe out, Object o) throws IOException, SerializeException { 097 if (o == null) 098 return; 099 var aType = getClassMetaForObject(o); 100 if (!aType.isBean() && !aType.isMap()) 101 throw new SerializeException(this, "INI format requires a bean or Map<String,?> at root. Got: %s", aType.inner().getName()); 102 var w = getIniWriter(out); 103 var eType = getExpectedRootType(o); 104 if (isAddBeanTypes() || isAddRootType()) { 105 var typeName = getBeanTypeName(this, eType, aType, null); 106 if (typeName == null) 107 typeName = aType.inner().getSimpleName(); 108 var typeKey = getBeanTypePropertyName(eType); 109 w.keyValue(typeKey, typeName); 110 } 111 if (aType.isBean()) 112 writeBean(w, toBeanMap(o), ""); 113 else 114 writeMapAtRoot(w, (Map<?,?>)o, eType); 115 } 116 117 protected final IniWriter getIniWriter(SerializerPipe out) { 118 var output = out.getRawOutput(); 119 if (output instanceof IniWriter output2) 120 return output2; 121 var w = new IniWriter(out.getWriter(), isUseWhitespace(), getMaxIndent(), isTrimStrings(), 122 ctx.kvSeparator, ctx.spacedSeparator, getUriResolver()); 123 out.setWriter(w); 124 return w; 125 } 126 127 private void writeBean(IniWriter w, BeanMap<?> m, String sectionPath) throws IOException, SerializeException { 128 Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x); 129 var simple = new ArrayList<Map.Entry<BeanPropertyMeta,Object>>(); 130 var sections = new ArrayList<Map.Entry<BeanPropertyMeta,Object>>(); 131 132 m.forEachValue(checkNull, (pMeta, key, value, thrown) -> { 133 if (nn(thrown)) 134 onBeanGetterException(pMeta, thrown); 135 var cMeta = (ClassMeta<?>) pMeta.getBeanInfo(); 136 if (canIgnoreValue(pMeta, key, value)) 137 return; 138 var aType = value == null ? cMeta : getClassMetaForObject(value, cMeta); 139 var iniMeta = ctx.getIniBeanPropertyMeta(pMeta); 140 // Collections/arrays are written as inline key-value (not sections), so they must appear in 141 // the default section before any [section] headers for correct parsing 142 if (iniMeta.isJson5Encoding() || isSimpleOrJson5Inline(aType) 143 || aType.isCollectionOrArrayOrOptional()) { 144 simple.add(new AbstractMap.SimpleEntry<>(pMeta, value)); 145 } else { 146 sections.add(new AbstractMap.SimpleEntry<>(pMeta, value)); 147 } 148 }); 149 150 // Pass 1: simple properties and JSON5-inline values 151 for (var e : simple) { 152 BeanPropertyMeta pMeta = e.getKey(); 153 if (ctx.useComments) { 154 var iniMeta = ctx.getIniBeanPropertyMeta(pMeta); 155 if (ine(iniMeta.getComment())) 156 w.comment(iniMeta.getComment()); 157 } 158 writeKeyValue(w, pMeta.getName(), e.getValue(), pMeta); 159 } 160 161 // Pass 2: bean and Map sections 162 for (var e : sections) { 163 BeanPropertyMeta pMeta = e.getKey(); 164 Object value = e.getValue(); 165 var iniMeta = ctx.getIniBeanPropertyMeta(pMeta); 166 var key = ine(iniMeta.getSection()) ? iniMeta.getSection() : pMeta.getName(); 167 var cMeta = (ClassMeta<?>) pMeta.getBeanInfo(); 168 var newPath = sectionPath.isEmpty() ? key : sectionPath + SECTION_PATH_DELIMITER + key; 169 170 if (nn(value)) { 171 if (isUseWhitespace() && !sectionPath.isEmpty()) 172 w.blankLine(); 173 var aType = getClassMetaForObject(value, cMeta); 174 if (aType.isBean()) { 175 w.section(newPath); 176 writeBean(w, toBeanMap(value), newPath); 177 } else if (aType.isMap()) { 178 var map = (Map<?,?>)value; 179 if (isSimpleMap(map, aType)) { 180 w.section(newPath); 181 writeMapSection(w, map); 182 } else { 183 if (ctx.useComments && ine(iniMeta.getComment())) 184 w.comment(iniMeta.getComment()); 185 writeKeyValue(w, pMeta.getName(), value, pMeta); 186 } 187 } else { 188 // Collections and arrays, plus anything that is neither bean, map, collection, nor array 189 // (e.g. a streamable-but-not-collection value such as a Stream). The latter isn't classified 190 // as "simple" at classification time, so it falls through to the "sections" pass here -- 191 // write it as an inline key-value rather than silently dropping it. 192 if (ctx.useComments && ine(iniMeta.getComment())) 193 w.comment(iniMeta.getComment()); 194 writeKeyValue(w, pMeta.getName(), value, pMeta); 195 } 196 } else if (isKeepNullProperties()) { 197 if (ctx.useComments && ine(iniMeta.getComment())) 198 w.comment(iniMeta.getComment()); 199 writeKeyValue(w, pMeta.getName(), null, pMeta); 200 } 201 } 202 } 203 204 private void writeMapAtRoot(IniWriter w, Map<?,?> map, ClassMeta<?> type) throws SerializeException { 205 Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x); 206 forEachEntry(map, e -> { 207 var k = toString(e.getKey()); 208 var v = e.getValue(); 209 if (!checkNull.test(v)) 210 return; 211 try { 212 var aType = getClassMetaForObject(v, type); 213 if (aType.isBean()) { 214 if (isUseWhitespace()) 215 w.blankLine(); 216 w.section(k); 217 writeBean(w, toBeanMap(v), k); 218 } else if (aType.isMap() && v != null) { 219 var nested = (Map<?,?>)v; 220 if (isSimpleMap(nested, aType)) { 221 if (isUseWhitespace()) 222 w.blankLine(); 223 w.section(k); 224 writeMapSection(w, nested); 225 } else { 226 writeKeyValue(w, k, v, null); 227 } 228 } else { 229 writeKeyValue(w, k, v, null); 230 } 231 } catch (SerializeException | IOException ex) { 232 throw rex(ex); 233 } 234 }); 235 } 236 237 private void writeMapSection(IniWriter w, Map<?,?> map) throws SerializeException { 238 Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x); 239 forEachEntry(map, e -> { 240 var k = toString(e.getKey()); 241 var v = e.getValue(); 242 if (!checkNull.test(v)) 243 return; 244 writeKeyValue(w, k, v, null); 245 }); 246 } 247 248 private void writeKeyValue(IniWriter w, String key, Object value, BeanPropertyMeta pMeta) throws SerializeException { 249 ClassMeta<?> cMeta; 250 if (pMeta != null) 251 cMeta = (ClassMeta<?>) pMeta.getBeanInfo(); 252 else if (value != null) 253 cMeta = getClassMetaForObject(value); 254 else 255 cMeta = object(); 256 var aType = value == null ? cMeta : getClassMetaForObject(value, cMeta); 257 258 var swap = aType.getSwap(this); 259 if (nn(swap)) { 260 value = swap(swap, value); 261 aType = swap.getSwapClassMeta(this); 262 if (aType.isObject()) 263 aType = getClassMetaForObject(value); 264 } 265 266 if (value == null && !isKeepNullProperties()) 267 return; 268 269 String valueStr; 270 if (value == null) 271 valueStr = "null"; 272 else if (isSimpleOrJson5Inline(aType)) 273 valueStr = formatSimpleValue(value, aType); 274 else 275 valueStr = encodeComplexValue(value); 276 w.keyValue(key, valueStr); 277 } 278 279 private String formatSimpleValue(Object value, ClassMeta<?> aType) throws SerializeException { 280 if (aType.isCharSequence() || aType.isUri()) { 281 var s = toString(value); 282 return needsQuoting(s) ? "'" + s.replace("'", "''") + "'" : s; 283 } 284 if (aType.isNumber()) 285 return value.toString(); 286 if (aType.isBoolean()) 287 return ((Boolean)value).toString(); 288 if (aType.isEnum()) 289 return ((Enum<?>)value).name(); 290 if (aType.isDate()) 291 return writeDate((Date)value, aType); 292 if (aType.isCalendar()) 293 return writeCalendar(value, aType); 294 if (aType.isTemporal()) 295 return writeTemporal((TemporalAccessor)value, aType); 296 if (aType.isDuration()) 297 return writeDuration((Duration)value); 298 if (aType.isPeriod()) 299 return writePeriod((Period)value); 300 return toString(value); 301 } 302 303 private static boolean needsQuoting(String s) { 304 if (ie(s)) 305 return true; 306 if (eq(s, "null") || eqic(s, "true") || eqic(s, "false")) 307 return true; 308 // Whole-string numeric check; mirrors -?\d+(\.\d+)?([eE][+-]?\d+)? (integer part allows leading zeros). 309 if (StringUtils.matchNumberPrefix(s, true) == s.length()) 310 return true; 311 if (s.contains("=") || s.contains("[") || s.contains("]") || s.contains("#") || s.contains("\n")) 312 return true; 313 var trimmed = s.trim(); 314 return !trimmed.equals(s); 315 } 316 317 private String encodeComplexValue(Object value) throws SerializeException { 318 return getJson5Serializer().write(value); 319 } 320 321 private JsonSerializer getJson5Serializer() { 322 var b = Json5Serializer.create().marshallingContext((MarshallingContext)getContext()); 323 if (isAddBeanTypes()) 324 b.addBeanTypes(); 325 if (isAddRootType()) 326 b.addRootType(); 327 return b.build(); 328 } 329 330 private static boolean isSimpleOrJson5Inline(ClassMeta<?> aType) { 331 if (aType.isBean() || aType.isMap()) 332 return false; 333 if (aType.isCollection() || aType.isArray()) 334 return false; 335 return !aType.isStreamable(); 336 } 337 338 private boolean isSimpleMap(Map<?,?> map, ClassMeta<?> mapType) { 339 if (map == null) 340 return false; 341 var keyType = mapType.getKeyType(); 342 // When keyType is null or Object, check keys at runtime; otherwise require CharSequence 343 if (keyType != null && !keyType.isCharSequence() && !keyType.isObject()) 344 return false; 345 for (Object k : map.keySet()) { 346 if (k != null && !(k instanceof CharSequence)) 347 return false; 348 } 349 for (Object v : map.values()) { 350 if (v != null) { 351 ClassMeta<?> vType = getClassMetaForObject(v); 352 if (vType.isBean() || vType.isMap() || vType.isCollectionOrArray()) 353 return false; 354 } 355 } 356 return true; 357 } 358}