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.hocon; 018 019import static org.apache.juneau.commons.utils.IoUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021 022import java.io.*; 023import java.time.*; 024import java.time.temporal.*; 025import java.util.*; 026import java.util.function.*; 027 028import org.apache.juneau.commons.bean.*; 029import org.apache.juneau.commons.lang.*; 030import org.apache.juneau.marshall.*; 031import org.apache.juneau.marshall.serializer.*; 032import org.apache.juneau.marshall.stream.*; 033 034/** 035 * Session for serializing objects to HOCON format. 036 */ 037@SuppressWarnings({ 038 "java:S110", // Inheritance depth comes from the inherited WriterSerializerSession session hierarchy, not from this class 039 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 040 "java:S3776", // writeBeanMap() and writeAnything() branch over every HOCON value kind (bean, map, collection, date, duration, stream) in one pass 041 "java:S6541", // writeAnything() is the single dispatch point over all serializable value kinds 042 "rawtypes", // writeMap()/writeCollection() take raw Map/Collection because values arrive as Object and are cast after ClassMeta type checks 043 "resource", // The HoconWriter created in getHoconWriter() wraps the pipe's writer and is closed by the SerializerPipe, not by this session 044 "unchecked" // Raw Map/Collection casts in doWrite()/writeAnything()/writeBeanMap() are guarded by the ClassMeta isMap()/isCollection() and instanceof checks 045}) 046public class HoconSerializerSession extends WriterSerializerSession implements RecordWritable { 047 048 /** 049 * Builder for HOCON serializer session. 050 */ 051 public static class Builder extends WriterSerializerSession.Builder<Builder> { 052 053 private final HoconSerializer ctx; 054 055 protected Builder(HoconSerializer ctx) { 056 super(reqnn("ctx", ctx)); 057 this.ctx = ctx; 058 } 059 060 @Override 061 public HoconSerializerSession build() { 062 return new HoconSerializerSession(this); 063 } 064 } 065 066 /** 067 * Creates a session builder. 068 * 069 * @param ctx The serializer context. 070 * <br>Cannot be <jk>null</jk>. 071 * @return The builder. 072 */ 073 public static Builder create(HoconSerializer ctx) { 074 return new Builder(reqnn("ctx", ctx)); 075 } 076 077 private final HoconSerializer ctx; 078 079 protected HoconSerializerSession(Builder builder) { 080 super(builder); 081 ctx = builder.ctx; 082 } 083 084 @Override /* RecordWritable */ 085 public RecordWriter writeRecords(Object output) throws IOException { 086 return RecordAdapter.writer(this, output); 087 } 088 089 @Override /* RecordWritable */ 090 public boolean isRecordStreaming() { 091 return false; 092 } 093 094 protected final HoconWriter getHoconWriter(SerializerPipe out) { 095 var output = out.getRawOutput(); 096 if (output instanceof HoconWriter output2) 097 return output2; 098 var w = new HoconWriter(out.getWriter(), isUseWhitespace(), getMaxIndent(), isTrimStrings(), 099 ctx.useNewlineSeparators, ctx.useUnquotedStrings, ctx.useUnquotedKeys, ctx.omitRootBraces, 100 ctx.useMultilineStrings, ctx.useEqualsSign, getUriResolver()); 101 out.setWriter(w); 102 return w; 103 } 104 105 @Override 106 protected void doWrite(SerializerPipe out, Object o) throws IOException, SerializeException { 107 if (o == null) 108 return; 109 var aType = getClassMetaForObject(o); 110 var eType = getExpectedRootType(o); 111 var hw = getHoconWriter(out).i(getInitialDepth()); 112 ClassMeta<?> sType = aType; 113 var swap = aType.getSwap(this); 114 if (nn(swap)) { 115 o = swap(swap, o); 116 sType = swap.getSwapClassMeta(this); 117 if (sType.isObject()) 118 sType = getClassMetaForObject(o); 119 } 120 if (sType.isBean() || sType.isMap()) { 121 var omitRoot = ctx.omitRootBraces; 122 if (sType.isBean()) 123 writeBeanMap(hw, toBeanMap(o), getBeanTypeName(this, eType, sType, null), omitRoot); 124 else 125 writeMap(hw, (Map) o, eType, omitRoot); 126 } else { 127 writeAnything(hw, o, eType, "root", null); 128 } 129 } 130 131 private void writeBeanMap(HoconWriter out, BeanMap<?> m, String typeName, boolean omitRootBraces) throws SerializeException { 132 var i = indent; 133 if (!omitRootBraces) { 134 out.objectStart(i); 135 indent++; 136 } 137 var first = Flag.create(); 138 Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x); 139 140 if (nn(typeName)) { 141 var pm = m.getMeta().getTypeProperty(); 142 writeKeyPrefix(out, pm.getName(), i, first); 143 out.equalsSign(); 144 writeString(out, typeName); 145 first.set(); 146 } 147 148 m.forEachValue(checkNull, (pMeta, key, value, thrown) -> { 149 var cMeta = (ClassMeta<?>) pMeta.getBeanInfo(); 150 if (nn(thrown)) 151 onBeanGetterException(pMeta, thrown); 152 if (canIgnoreValue(pMeta, key, value)) 153 return; 154 var isObject = cMeta.isBean() || cMeta.isMap(); 155 writeKeyPrefix(out, key, i, first); 156 if (isObject) { 157 out.append(" "); 158 if (value instanceof BeanMap value2) 159 writeBeanMap(out, value2, getBeanTypeName(this, cMeta, cMeta, pMeta), false); 160 else if (cMeta.isBean()) 161 writeBeanMap(out, toBeanMap(value), getBeanTypeName(this, cMeta, cMeta, pMeta), false); 162 else 163 writeMap(out, (Map) value, cMeta, false); 164 } else { 165 out.equalsSign(); 166 writeAnything(out, value, cMeta, key, pMeta); 167 } 168 first.set(); 169 }); 170 171 if (!omitRootBraces) { 172 indent--; 173 out.objectEnd(indent); 174 } 175 } 176 177 private void writeKeyPrefix(HoconWriter out, Object key, int depth, Flag first) throws SerializeException { 178 if (first.isSet()) 179 out.separator(depth); 180 else 181 out.cr(depth); 182 var keyStr = toString(key); 183 out.key(keyStr); 184 } 185 186 private void writeMap(HoconWriter out, Map m, ClassMeta<?> type, boolean omitRootBraces) throws SerializeException { 187 var i = indent; 188 if (!omitRootBraces) { 189 out.objectStart(i); 190 indent++; 191 } 192 var keyType = type.getKeyType(); 193 var valueType = type.getValueType(); 194 var first = Flag.create(); 195 196 forEachEntry(m, x -> { 197 var value = x.getValue(); 198 var key = generalize(x.getKey(), keyType); 199 var isObject = value != null && (value instanceof Map || value instanceof BeanMap); 200 writeKeyPrefix(out, key, i, first); 201 if (isObject) { 202 out.append(" "); 203 if (value instanceof BeanMap value2) 204 writeBeanMap(out, value2, getBeanTypeName(this, valueType, getClassMetaForObject(value), null), false); 205 else 206 writeMap(out, (Map) value, object(), false); 207 } else { 208 out.equalsSign(); 209 writeAnything(out, value, valueType, key == null ? null : toString(key), null); 210 } 211 first.set(); 212 }); 213 214 if (!omitRootBraces) { 215 indent--; 216 out.objectEnd(indent); 217 } 218 } 219 220 private void writeCollection(HoconWriter out, Collection c, ClassMeta<?> type) throws SerializeException { 221 var eType = type.getElementType(); 222 out.arrayStart(); 223 indent++; 224 var first = Flag.create(); 225 forEachEntry(c, x -> { 226 if (first.isSet()) 227 out.separator(indent); 228 else 229 out.cr(indent); 230 writeAnything(out, x, eType, "<iterator>", null); 231 first.set(); 232 }); 233 indent--; 234 out.arrayEnd(); 235 } 236 237 private void writeString(HoconWriter out, String s) throws SerializeException { 238 if (s == null) { 239 out.w("null"); 240 return; 241 } 242 if (ctx.useMultilineStrings && s.contains("\n")) 243 out.tripleQuotedString(s); 244 else if (out.isSimpleValue(s)) 245 out.unquotedString(s); 246 else 247 out.quotedString(s); 248 } 249 250 protected HoconWriter writeAnything(HoconWriter hw, Object o, ClassMeta<?> eType, String attrName, BeanPropertyMeta pMeta) throws SerializeException { 251 if (o == null) { 252 hw.w("null"); 253 return hw; 254 } 255 if (eType == null) 256 eType = object(); 257 258 var aType = push2(attrName, o, eType); 259 var isRecursion = aType == null; 260 261 if (aType == null) { 262 o = null; 263 aType = object(); 264 } 265 266 if (isOptional(aType)) { 267 o = getOptionalValue(o); 268 eType = getOptionalType(eType); 269 aType = getClassMetaForObject(o, object()); 270 } 271 272 var sType = aType; 273 var swap = aType.getSwap(this); 274 if (nn(swap)) { 275 o = swap(swap, o); 276 sType = swap.getSwapClassMeta(this); 277 if (sType.isObject()) 278 sType = getClassMetaForObject(o); 279 } 280 281 if (o == null || (sType.isChar() && ((Character) o).charValue() == 0)) { 282 hw.w("null"); 283 } else if (o instanceof byte[] o2) { 284 // byte[] gate: encode as Base64 before isArray would otherwise route it as a generic array. 285 writeString(hw, Base64.getEncoder().encodeToString(o2)); 286 } else if (sType.isBean()) { 287 writeBeanMap(hw, toBeanMap(o), getBeanTypeName(this, eType, aType, pMeta), false); 288 } else if (sType.isMap()) { 289 if (sType.isBeanMap()) 290 writeBeanMap(hw, (BeanMap) o, getBeanTypeName(this, eType, aType, pMeta), false); 291 else 292 writeMap(hw, (Map) o, eType, false); 293 } else if (sType.isCollection()) { 294 writeCollection(hw, (Collection) o, eType); 295 } else if (sType.isArray()) { 296 writeCollection(hw, toList(sType.inner(), o), eType); 297 } else if (sType.isNumber() || sType.isBoolean()) { 298 hw.append(o); 299 } else if (sType.isUri() || (nn(pMeta) && pMeta.isUri())) { 300 writeString(hw, getUriResolver().resolve(o)); 301 } else if (sType.isDate()) { 302 writeString(hw, writeDate((Date)o, sType)); 303 } else if (sType.isCalendar()) { 304 writeString(hw, writeCalendar(o, sType)); 305 } else if (sType.isTemporal()) { 306 writeString(hw, writeTemporal((TemporalAccessor)o, sType)); 307 } else if (sType.isDuration()) { 308 var value = writeDuration((Duration)o); 309 if (getDurationFormat().isNumeric()) 310 hw.append(value); 311 else 312 writeString(hw, value); 313 } else if (sType.isPeriod()) { 314 writeString(hw, writePeriod((Period)o)); 315 } else if (sType.isStreamable()) { 316 writeStreamable(hw, o, sType, eType); 317 } else if (sType.isReader()) { 318 pipe((Reader) o, hw, SerializerSession::handleThrown); 319 } else if (sType.isInputStream()) { 320 pipe((InputStream) o, hw, SerializerSession::handleThrown); 321 } else { 322 writeString(hw, toString(o)); 323 } 324 325 if (!isRecursion) 326 pop(); 327 return hw; 328 } 329 330 private void writeStreamable(HoconWriter out, Object o, ClassMeta<?> sType, ClassMeta<?> type) throws SerializeException { 331 var eType = type.getElementType(); 332 out.arrayStart(); 333 indent++; 334 var first = Flag.create(); 335 forEachStreamableEntry(o, sType, x -> { 336 if (first.isSet()) 337 out.separator(indent); 338 else 339 out.cr(indent); 340 writeAnything(out, x, eType, "<iterator>", null); 341 first.set(); 342 }); 343 indent--; 344 out.arrayEnd(); 345 } 346}