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.cbor; 018 019import static org.apache.juneau.commons.utils.CollectionUtils.*; 020import static org.apache.juneau.commons.utils.IoUtils.*; 021import static org.apache.juneau.commons.utils.Shorts.*; 022 023import java.io.*; 024import java.time.*; 025import java.time.temporal.*; 026import java.util.*; 027import java.util.function.*; 028 029import org.apache.juneau.commons.bean.*; 030import org.apache.juneau.marshall.*; 031import org.apache.juneau.marshall.serializer.*; 032import org.apache.juneau.marshall.stream.*; 033 034/** 035 * Session object that lives for the duration of a single use of {@link CborSerializer}. 036 * 037 * <h5 class='section'>Notes:</h5><ul> 038 * <li class='warn'>This class is not thread safe and is typically discarded after one use. 039 * </ul> 040 * 041 * <h5 class='section'>See Also:</h5><ul> 042 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Cbor">CBOR</a> 043 * </ul> 044 */ 045@SuppressWarnings({ 046 "java:S110", // Inheritance depth acceptable for serializer session hierarchy 047 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 048 "rawtypes", // Raw BeanMap/Map/Collection casts in writeAnything() and the writeMap()/writeCollection() helpers, where element types are only known at runtime. 049 "resource", // Output streams managed by calling code 050 "unchecked" // Type erasure requires unchecked operations 051}) 052public class CborSerializerSession extends OutputStreamSerializerSession implements TokenWritable, ArrayRecordWritable { 053 054 /** 055 * Builder class. 056 */ 057 public static class Builder extends OutputStreamSerializerSession.Builder<Builder> { 058 059 /** 060 * Constructor 061 * 062 * @param ctx The context creating this session. 063 * <br>Cannot be <jk>null</jk>. 064 */ 065 protected Builder(CborSerializer ctx) { 066 super(reqnn("ctx", ctx)); 067 } 068 069 @Override 070 public CborSerializerSession build() { 071 return new CborSerializerSession(this); 072 } 073 074 } 075 076 private record SimpleMapEntry(Object key, Object value) {} 077 078 /** 079 * Creates a new builder for this object. 080 * 081 * @param ctx The context creating this session. 082 * <br>Cannot be <jk>null</jk>. 083 * @return A new builder. 084 */ 085 public static Builder create(CborSerializer ctx) { 086 return new Builder(reqnn("ctx", ctx)); 087 } 088 089 /** 090 * Converts the specified output target object to an {@link CborOutputStream}. 091 */ 092 private static CborOutputStream getCborOutputStream(SerializerPipe out) throws IOException { 093 Object output = out.getRawOutput(); 094 if (output instanceof CborOutputStream output2) 095 return output2; 096 var os = new CborOutputStream(out.getOutputStream()); 097 out.setOutputStream(os); 098 return os; 099 } 100 101 /** 102 * Constructor. 103 * 104 * @param builder The builder for this object. 105 */ 106 protected CborSerializerSession(Builder builder) { 107 super(builder); 108 } 109 110 /** 111 * Opens a low-level push generator that emits CBOR (RFC 8949) one structural event at a time, 112 * bound to this live session. 113 * 114 * <p> 115 * Containers ({@code startObject} / {@code startArray}) are emitted as CBOR 116 * <b>indefinite-length</b> markers (<c>0xBF</c> / <c>0x9F</c>) so the caller doesn't have to 117 * know element counts up front; matching {@code endXxx} calls emit the BREAK byte 118 * (<c>0xFF</c>). Resulting bytes are valid RFC-8949 CBOR. 119 * 120 * <h5 class='section'>Builder properties honored:</h5> 121 * Databind-level settings flow through to {@link TokenWriter#object(Object) object(Object)}: 122 * <c>keepNullProperties</c>, <c>trimEmptyMaps</c>, <c>trimEmptyCollections</c>, 123 * <c>sortMaps</c>, <c>sortCollections</c>, <c>trimStrings</c>. 124 * 125 * <h5 class='section'>Builder properties NOT honored:</h5> 126 * <c>useWhitespace</c>, <c>maxIndent</c>, <c>quoteChar</c>, <c>escapeSolidus</c> (binary 127 * format — no text-formatting concepts apply); <c>uriContext</c>, 128 * <c>uriResolution</c>, <c>uriRelativity</c>, <c>listener</c>. 129 * 130 * @param output The output. Accepts an {@link OutputStream}. 131 * @return A new {@link CborTokenWriter}. 132 * @throws IOException If the output type is not supported or could not be opened. 133 */ 134 @Override /* TokenWritable */ 135 public TokenWriter writeTokens(Object output) throws IOException { 136 if (output == null) 137 throw new IOException("Output cannot be null."); 138 if (!(output instanceof OutputStream output2)) 139 throw ioex("Cannot convert object of type %s to an OutputStream.", output.getClass().getName()); 140 var walk = new PojoWalker.Options( 141 isKeepNullProperties(), 142 isTrimEmptyMaps(), 143 isTrimEmptyCollections(), 144 isSortMaps(), 145 isSortCollections(), 146 isTrimStrings(), 147 getMarshallingContext()); 148 return new CborTokenWriter(output2, new CborTokenWriter.Settings(walk)); 149 } 150 151 /** 152 * Streaming array-element {@link RecordWriter} backed by {@link CborTokenWriter}. 153 * Memory is O(1) — CBOR's writer always uses indefinite-length arrays 154 * (0x9F .. 0xFF) so element count is not required up front. 155 * 156 * @param output The output. 157 * @return A new element-streamed {@link RecordWriter}. 158 * @throws IOException If a problem occurred opening the underlying output. 159 */ 160 @Override /* ArrayRecordWritable */ 161 public RecordWriter writeArrayRecords(Object output) throws IOException { 162 return StreamingArrayRecord.writer(writeTokens(output)); 163 } 164 165 /* 166 * Workhorse method. 167 * Determines the type of object, and then calls the appropriate type-specific serialization method. 168 */ 169 @SuppressWarnings({ 170 "java:S3776" // Cognitive complexity acceptable for serialization dispatch logic 171 }) 172 private CborOutputStream writeAnything(CborOutputStream out, Object o, ClassMeta<?> eType, String attrName, BeanPropertyMeta pMeta) throws SerializeException { 173 174 if (o == null) 175 return out.appendNull(); 176 177 if (eType == null) 178 eType = object(); 179 180 ClassMeta<?> aType = null; 181 ClassMeta<?> sType = null; 182 183 aType = push2(attrName, o, eType); 184 185 if (aType == null) 186 return out.appendNull(); 187 188 if (isOptional(aType)) { 189 o = getOptionalValue(o); 190 eType = getOptionalType(eType); 191 aType = getClassMetaForObject(o, object()); 192 } 193 194 sType = aType; 195 String typeName = getBeanTypeName(this, eType, aType, pMeta); 196 197 var swap = aType.getSwap(this); 198 if (nn(swap)) { 199 o = swap(swap, o); 200 sType = swap.getSwapClassMeta(this); 201 202 if (sType.isObject()) 203 sType = getClassMetaForObject(o); 204 } 205 206 if (o == null || (sType.isChar() && ((Character)o).charValue() == 0)) 207 out.appendNull(); 208 else if (sType.isBean()) 209 writeBeanMap(out, toBeanMap(o), typeName); 210 else if (sType.isMap()) { 211 if (sType.isBeanMap()) 212 writeBeanMap(out, (BeanMap)o, typeName); 213 else 214 writeMap(out, (Map)o, eType); 215 } else if (sType.isCollection()) { 216 writeCollection(out, (Collection)o, eType); 217 } else if (sType.isByteArray()) { 218 var binaryFormat = getBinaryFormat(); 219 if (binaryFormat == BinaryFormat.NOT_SET) 220 out.appendBinary((byte[])o); 221 else 222 out.appendString(binaryFormat.format((byte[])o)); 223 } else if (sType.isArray()) { 224 writeCollection(out, toList(sType.inner(), o), eType); 225 } else if (sType.isBoolean()) { 226 out.appendBoolean((Boolean)o); 227 } else if (sType.isNumber()) { 228 out.appendNumber((Number)o); 229 } else if (sType.isUri() || (nn(pMeta) && pMeta.isUri())) { 230 out.appendString(resolveUri(o.toString())); 231 } else if (sType.isDate()) { 232 out.appendString(writeDate((Date)o, sType)); 233 } else if (sType.isCalendar()) { 234 out.appendString(writeCalendar(o, sType)); 235 } else if (sType.isTemporal()) { 236 out.appendString(writeTemporal((TemporalAccessor)o, sType)); 237 } else if (sType.isDuration()) { 238 appendDuration(out, (Duration)o); 239 } else if (sType.isPeriod()) { 240 out.appendString(writePeriod((Period)o)); 241 } else if (sType.isStreamable()) { 242 writeCollection(out, toListFromStreamable(o, sType), eType); 243 } else if (sType.isReader()) { 244 pipe((Reader)o, out, SerializerSession::handleThrown); 245 } else if (sType.isInputStream()) { 246 pipe((InputStream)o, out, SerializerSession::handleThrown); 247 } else 248 out.appendString(toString(o)); 249 250 pop(); 251 return out; 252 } 253 254 private void writeBeanMap(CborOutputStream out, BeanMap<?> m, String typeName) throws SerializeException { 255 256 Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x); 257 258 var values = new ArrayList<BeanPropertyValue>(); 259 260 if (nn(typeName)) { 261 BeanPropertyMeta pm = m.getMeta().getTypeProperty(); 262 values.add(new BeanPropertyValue(pm, pm.getName(), typeName, null)); 263 } 264 265 m.forEachValue(checkNull, (pMeta, key, value, thrown) -> { 266 if (nn(thrown)) { 267 onBeanGetterException(pMeta, thrown); 268 return; 269 } 270 var p = new BeanPropertyValue(pMeta, key, value, null); 271 272 if ((! isKeepNullProperties()) && willRecurse(p)) { 273 return; 274 } 275 276 values.add(p); 277 }); 278 279 out.startMap(values.size()); 280 281 values.forEach(x -> { 282 BeanPropertyMeta pMeta = x.getMeta(); 283 if (pMeta.canRead()) { 284 ClassMeta<?> cMeta = (ClassMeta<?>) x.getBeanInfo(); 285 String key = x.getName(); 286 Object value = x.getValue(); 287 writeAnything(out, key, null, null, null); 288 writeAnything(out, value, cMeta, key, pMeta); 289 } 290 }); 291 } 292 293 private void writeCollection(CborOutputStream out, Collection c, ClassMeta<?> type) throws SerializeException { 294 var elementType = type.getElementType(); 295 List<Object> l = listOfSize(c.size()); 296 c = sort(c); 297 l.addAll(c); 298 out.startArray(l.size()); 299 l.forEach(x -> writeAnything(out, x, elementType, "<iterator>", null)); 300 } 301 302 private void writeMap(CborOutputStream out, Map m, ClassMeta<?> type) throws SerializeException { 303 304 var keyType = type.getKeyType(); 305 var valueType = type.getValueType(); 306 307 m = sort(m); 308 309 List<SimpleMapEntry> entries = listOfSize(m.size()); 310 m.forEach((k, v) -> entries.add(new SimpleMapEntry(k, v))); 311 312 out.startMap(entries.size()); 313 314 entries.forEach(x -> { 315 Object value = x.value(); 316 Object key = generalize(x.key(), keyType); 317 writeAnything(out, key, keyType, null, null); 318 writeAnything(out, value, valueType, null, null); 319 }); 320 } 321 322 private boolean willRecurse(BeanPropertyValue v) throws SerializeException { 323 var aType = push2(v.getName(), v.getValue(), (ClassMeta<?>) v.getBeanInfo()); 324 if (nn(aType)) 325 pop(); 326 return aType == null; 327 } 328 329 @Override /* Overridden from SerializerSession */ 330 protected void doWrite(SerializerPipe out, Object o) throws IOException, SerializeException { 331 writeAnything(getCborOutputStream(out), o, getExpectedRootType(o), "root", null); 332 } 333 334 private void appendDuration(CborOutputStream out, Duration value) { 335 var f = getDurationFormat(); 336 var s = writeDuration(value); 337 if (f == DurationFormat.NANOS || f == DurationFormat.MILLIS) 338 out.appendNumber(Long.parseLong(s)); 339 else if (f == DurationFormat.SECONDS) 340 out.appendNumber(Double.parseDouble(s)); 341 else 342 out.appendString(s); 343 } 344 345}