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.yaml; 018 019import static org.apache.juneau.commons.utils.IoUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021 022import java.io.*; 023import java.util.*; 024import java.util.function.*; 025 026import org.apache.juneau.commons.bean.*; 027import org.apache.juneau.commons.lang.*; 028import org.apache.juneau.marshall.*; 029import org.apache.juneau.marshall.serializer.*; 030import org.apache.juneau.marshall.stream.*; 031 032/** 033 * Session object that lives for the duration of a single use of {@link YamlSerializer}. 034 * 035 * <h5 class='section'>Notes:</h5><ul> 036 * <li class='warn'>This class is not thread safe and is typically discarded after one use. 037 * </ul> 038 * 039 * <h5 class='section'>See Also:</h5><ul> 040 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/YamlSupport">YAML</a> 041 * </ul> 042 */ 043@SuppressWarnings({ 044 "java:S110", // Inheritance depth acceptable for this class hierarchy 045 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 046 "java:S3776", // Cognitive complexity acceptable for YAML serializer session methods 047 "rawtypes", // writeMap()/writeCollection() take raw Map/Collection and writeAnything() casts to raw BeanMap/Map/Collection after ClassMeta type checks 048 "resource", // Resource management handled externally 049 "unchecked" // Raw-to-generic casts in writeAnything()/writeMap()/writeCollection() are guarded by the ClassMeta isMap()/isCollection() checks 050}) 051public class YamlSerializerSession extends WriterSerializerSession implements RecordWritable, ArrayRecordWritable { 052 053 /** 054 * Builder class. 055 */ 056 public static class Builder extends WriterSerializerSession.Builder<Builder> { 057 058 /** 059 * Constructor 060 * 061 * @param ctx The context creating this session. 062 * <br>Cannot be <jk>null</jk>. 063 */ 064 protected Builder(YamlSerializer ctx) { 065 super(reqnn("ctx", ctx)); 066 } 067 068 @Override 069 public YamlSerializerSession build() { 070 return new YamlSerializerSession(this); 071 } 072 073 } 074 075 /** 076 * Creates a new builder for this object. 077 * 078 * @param ctx The context creating this session. 079 * <br>Cannot be <jk>null</jk>. 080 * @return A new builder. 081 */ 082 public static Builder create(YamlSerializer ctx) { 083 return new Builder(reqnn("ctx", ctx)); 084 } 085 086 /** 087 * Constructor. 088 * 089 * @param builder The builder for this object. 090 */ 091 protected YamlSerializerSession(Builder builder) { 092 super(builder); 093 } 094 095 @Override /* Overridden from SerializerSession */ 096 protected void doWrite(SerializerPipe out, Object o) throws IOException, SerializeException { 097 writeAnything(getYamlWriter(out), o, getExpectedRootType(o), "root", null, true); 098 } 099 100 /** 101 * Opens a whole-value push generator targeting YAML output, bound to this live session. 102 * {@link RecordWriter#write(Object) write(Object)} delegates to 103 * {@link SerializerSession#write(Object, Object)}. 104 * 105 * @param output The output. 106 * @return A new {@link RecordWriter}. 107 * @throws IOException If a problem occurred opening the underlying output. 108 */ 109 @Override /* RecordWritable */ 110 public RecordWriter writeRecords(Object output) throws IOException { 111 return RecordAdapter.writer(this, output); 112 } 113 114 /** 115 * Buffered array-element {@link RecordWriter} for YAML, bound to this live session. Buffers 116 * each {@code write(...)} and emits the whole list on {@code close()}. 117 * 118 * @param output The output. 119 * @return A new {@link RecordWriter}. 120 * @throws IOException If a problem occurred opening the underlying output. 121 */ 122 @Override /* ArrayRecordWritable */ 123 public RecordWriter writeArrayRecords(Object output) throws IOException { 124 return RecordAdapter.arrayWriter(this, output); 125 } 126 127 /** 128 * The YAML record writer is buffered/{@link RecordAdapter}-backed, not O(1) streaming. 129 * 130 * @return Always <jk>false</jk>. 131 */ 132 @Override /* RecordWritable */ 133 public boolean isRecordStreaming() { return false; } 134 135 /** 136 * The YAML array-record writer is buffered/{@link RecordAdapter}-backed, not O(1) streaming. 137 * 138 * @return Always <jk>false</jk>. 139 */ 140 @Override /* ArrayRecordWritable */ 141 public boolean isArrayRecordStreaming() { return false; } 142 143 /** 144 * Converts the specified output target object to a {@link YamlWriter}. 145 * 146 * @param out The output target object. 147 * <br>Must not be <jk>null</jk>. 148 * @return The output target object wrapped in a {@link YamlWriter}. 149 */ 150 protected final YamlWriter getYamlWriter(SerializerPipe out) { 151 var output = out.getRawOutput(); 152 if (output instanceof YamlWriter output2) 153 return output2; 154 var w = new YamlWriter(out.getWriter(), isUseWhitespace(), getMaxIndent(), isTrimStrings(), getUriResolver()); 155 out.setWriter(w); 156 return w; 157 } 158 159 /** 160 * Workhorse method. 161 * Determines the type of object, and then calls the appropriate type-specific serialization method. 162 * 163 * @param out The output writer. 164 * @param o The object to serialize. 165 * @param eType The expected type. 166 * @param attrName The attribute name. 167 * @param pMeta The bean property currently being parsed. 168 * @param asField If <jk>true</jk>, this value appears inline after a key or at root level. 169 * If <jk>false</jk>, this value needs its own indentation as a standalone block. 170 * @return The same writer passed in. 171 * @throws SerializeException General serialization error occurred. 172 */ 173 protected YamlWriter writeAnything(YamlWriter out, Object o, ClassMeta<?> eType, String attrName, BeanPropertyMeta pMeta, boolean asField) throws SerializeException { 174 175 if (o == null) { 176 out.append("null"); 177 return out; 178 } 179 180 if (eType == null) 181 eType = object(); 182 183 ClassMeta<?> aType; 184 ClassMeta<?> sType; 185 186 aType = push2(attrName, o, eType); 187 boolean isRecursion = aType == null; 188 189 if (aType == null) { 190 o = null; 191 aType = object(); 192 } 193 194 if (isOptional(aType)) { 195 o = getOptionalValue(o); 196 eType = getOptionalType(eType); 197 aType = getClassMetaForObject(o, object()); 198 } 199 200 sType = aType; 201 String typeName = getBeanTypeName(this, eType, aType, pMeta); 202 203 var swap = aType.getSwap(this); 204 if (nn(swap)) { 205 o = swap(swap, o); 206 sType = swap.getSwapClassMeta(this); 207 208 if (sType.isObject()) 209 sType = getClassMetaForObject(o); 210 } 211 212 if (o == null || (sType.isChar() && ((Character)o).charValue() == 0)) { 213 out.append("null"); 214 } else if (sType.isBean()) { 215 writeBeanMap(out, toBeanMap(o), typeName); 216 } else if (sType.isMap()) { 217 if (sType.isBeanMap()) 218 writeBeanMap(out, (BeanMap)o, typeName); 219 else 220 writeMap(out, (Map)o, eType); 221 } else if (sType.isCollection()) { 222 writeCollection(out, (Collection)o, eType, asField); 223 } else if (sType.isArray()) { 224 writeCollection(out, toList(sType.inner(), o), eType, asField); 225 } else if (sType.isNumber() || sType.isBoolean()) { 226 out.append(o); 227 } else if (sType.isUri() || (nn(pMeta) && pMeta.isUri())) { 228 out.uriValue(o); 229 } else if (sType.isReader()) { 230 pipe((Reader)o, out, SerializerSession::handleThrown); 231 } else if (sType.isInputStream()) { 232 pipe((InputStream)o, out, SerializerSession::handleThrown); 233 } else { 234 out.textValue(toString(o)); 235 } 236 237 if (! isRecursion) 238 pop(); 239 return out; 240 } 241 242 /** 243 * Determines whether a ClassMeta represents a complex type that requires block-style YAML formatting. 244 */ 245 private boolean isComplexValue(ClassMeta<?> cm) { 246 if (cm == null) 247 return false; 248 if (cm.isMapOrBean() || cm.isCollectionOrArrayOrOptional()) 249 return true; 250 var swap = cm.getSwap(this); 251 if (swap != null) { 252 var swapType = swap.getSwapClassMeta(this); 253 return swapType.isMapOrBean() || swapType.isCollectionOrArrayOrOptional(); 254 } 255 return false; 256 } 257 258 private SerializerWriter<?> writeBeanMap(YamlWriter out, BeanMap<?> m, String typeName) throws SerializeException { 259 int i = indent - 1; 260 261 var addAtt = Flag.create(); 262 Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x); 263 264 if (nn(typeName)) { 265 BeanPropertyMeta pm = m.getMeta().getTypeProperty(); 266 out.key(pm.getName()).textValue(typeName); 267 addAtt.set(); 268 } 269 270 m.forEachValue(checkNull, (pMeta, key, value, thrown) -> { 271 var cMeta = (ClassMeta<?>) pMeta.getBeanInfo(); 272 if (nn(thrown)) 273 onBeanGetterException(pMeta, thrown); 274 275 if (canIgnoreValue(pMeta, key, value)) 276 return; 277 278 addAtt.ifSet(() -> out.yamlIndent(i)).set(); 279 280 ClassMeta<?> actualType = value == null ? cMeta : getClassMetaForObject(value, cMeta); 281 if (value != null && isComplexValue(actualType)) { 282 out.keyName(key).w(": "); 283 if (actualType.isCollectionOrArrayOrOptional()) { 284 writeAnything(out, value, cMeta, key, pMeta, false); 285 } else { 286 out.yamlIndent(i + 1); 287 writeAnything(out, value, cMeta, key, pMeta, true); 288 } 289 } else { 290 out.key(key); 291 writeAnything(out, value, cMeta, key, pMeta, true); 292 } 293 }); 294 295 if (addAtt.isUnset()) 296 out.w("{}"); 297 298 return out; 299 } 300 301 private SerializerWriter<?> writeMap(YamlWriter out, Map m, ClassMeta<?> type) throws SerializeException { 302 int i = indent - 1; 303 304 var keyType = type.getKeyType(); 305 var valueType = type.getValueType(); 306 307 if (m.isEmpty()) { 308 out.w("{}"); 309 return out; 310 } 311 312 var addAtt = Flag.create(); 313 forEachEntry(m, x -> { 314 addAtt.ifSet(() -> out.yamlIndent(i)).set(); 315 Object value = x.getValue(); 316 Object key = generalize(x.getKey(), keyType); 317 318 ClassMeta<?> actualType = value == null ? valueType : getClassMetaForObject(value, valueType); 319 if (value != null && isComplexValue(actualType)) { 320 out.keyName(toString(key)).w(": "); 321 if (actualType.isCollectionOrArrayOrOptional()) { 322 writeAnything(out, value, valueType, (key == null ? null : toString(key)), null, false); 323 } else { 324 out.yamlIndent(i + 1); 325 writeAnything(out, value, valueType, (key == null ? null : toString(key)), null, true); 326 } 327 } else { 328 out.key(toString(key)); 329 writeAnything(out, value, valueType, (key == null ? null : toString(key)), null, true); 330 } 331 }); 332 333 return out; 334 } 335 336 private SerializerWriter<?> writeCollection(YamlWriter out, Collection c, ClassMeta<?> type, boolean asField) throws SerializeException { 337 int i = indent - 1; 338 339 if (c.isEmpty()) { 340 out.w("[]"); 341 return out; 342 } 343 344 var elementType = type.getElementType(); 345 346 var addEntry = Flag.create(); 347 forEachEntry(c, x -> { 348 if (asField && addEntry.isUnset()) { 349 out.w("- "); 350 } else { 351 out.listEntry(i); 352 } 353 addEntry.set(); 354 writeAnything(out, x, elementType, "<iterator>", null, true); 355 }); 356 357 return out; 358 } 359 360}