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.msgpack; 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 MsgPackSerializer}. 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/MessagePackSupport">MessagePack</a> 043 044 * </ul> 045 */ 046@SuppressWarnings({ 047 "java:S110", // Inheritance depth acceptable for serializer session hierarchy 048 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 049 "rawtypes", // writeMap()/writeCollection() take raw Map/Collection because values arrive as Object and are cast after ClassMeta type checks 050 "resource", // Output streams managed by calling code 051 "unchecked" // Type erasure requires unchecked operations 052}) 053public class MsgPackSerializerSession extends OutputStreamSerializerSession implements TokenWritable, ArrayRecordWritable { 054 055 /** 056 * Builder class. 057 */ 058 public static class Builder extends OutputStreamSerializerSession.Builder<Builder> { 059 060 /** 061 * Constructor 062 * 063 * @param ctx The context creating this session. 064 * <br>Cannot be <jk>null</jk>. 065 */ 066 protected Builder(MsgPackSerializer ctx) { 067 super(reqnn("ctx", ctx)); 068 } 069 070 @Override 071 public MsgPackSerializerSession build() { 072 return new MsgPackSerializerSession(this); 073 } 074 075 } 076 077 private record SimpleMapEntry(Object key, Object value) {} 078 079 /** 080 * Creates a new builder for this object. 081 * 082 * @param ctx The context creating this session. 083 * <br>Cannot be <jk>null</jk>. 084 * @return A new builder. 085 */ 086 public static Builder create(MsgPackSerializer ctx) { 087 return new Builder(reqnn("ctx", ctx)); 088 } 089 090 /** 091 * Converts the specified output target object to an {@link MsgPackOutputStream}. 092 */ 093 private static MsgPackOutputStream getMsgPackOutputStream(SerializerPipe out) throws IOException { 094 Object output = out.getRawOutput(); 095 if (output instanceof MsgPackOutputStream output2) 096 return output2; 097 var os = new MsgPackOutputStream(out.getOutputStream()); 098 out.setOutputStream(os); 099 return os; 100 } 101 102 /** 103 * Constructor. 104 * 105 * @param builder The builder for this object. 106 */ 107 protected MsgPackSerializerSession(Builder builder) { 108 super(builder); 109 } 110 111 /** 112 * Opens a low-level push generator that emits MessagePack one structural event at a time, 113 * bound to this live session. 114 * 115 * <p> 116 * MsgPack containers are length-prefixed with no indefinite-length encoding, so the writer 117 * <b>buffers</b> each open container's body bytes until the matching {@code endXxx} call; 118 * memory cost is O(largest open container). 119 * 120 * <h5 class='section'>Builder properties honored:</h5> 121 * <c>keepNullProperties</c>, <c>trimEmptyMaps</c>, <c>trimEmptyCollections</c>, <c>sortMaps</c>, 122 * <c>sortCollections</c>, <c>trimStrings</c> — all flow through to 123 * {@link TokenWriter#object(Object) object(Object)}. 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); <c>uriContext</c>, <c>uriResolution</c>, <c>uriRelativity</c>, <c>listener</c>. 128 * 129 * @param output The output. 130 * @return A new {@link MsgPackTokenWriter}. 131 * @throws IOException If the output type is not supported or could not be opened. 132 */ 133 @Override /* TokenWritable */ 134 public TokenWriter writeTokens(Object output) throws IOException { 135 if (output == null) 136 throw new IOException("Output cannot be null."); 137 if (!(output instanceof OutputStream output2)) 138 throw ioex("Cannot convert object of type %s to an OutputStream.", output.getClass().getName()); 139 var walk = new PojoWalker.Options( 140 isKeepNullProperties(), 141 isTrimEmptyMaps(), 142 isTrimEmptyCollections(), 143 isSortMaps(), 144 isSortCollections(), 145 isTrimStrings(), 146 getMarshallingContext()); 147 return new MsgPackTokenWriter(output2, new MsgPackTokenWriter.Settings(walk)); 148 } 149 150 /** 151 * Buffered array-element {@link RecordWriter}. MsgPack's array wire format is 152 * length-prefixed, so streaming without a known element count requires either buffering 153 * (this method) or the {@link #writeArrayRecords(Object, int)} overload that takes a count. 154 * 155 * @param output The output. 156 * @return A buffered {@link RecordWriter}. 157 * @throws IOException If a problem occurred opening the underlying output. 158 */ 159 @Override /* ArrayRecordWritable */ 160 public RecordWriter writeArrayRecords(Object output) throws IOException { 161 return RecordAdapter.arrayWriter(this, output); 162 } 163 164 @Override /* ArrayRecordWritable */ 165 public boolean isArrayRecordStreaming() { return false; } 166 167 /** 168 * Streaming array-element {@link RecordWriter} that pre-declares the element count and 169 * writes each element directly to the output stream — no per-element buffering. 170 * The caller must pass exactly {@code expectedCount} {@link RecordWriter#write(Object)} 171 * calls before {@link RecordWriter#close()}; mismatch throws on close. 172 * 173 * @param output The output (must be an {@link OutputStream}). 174 * @param expectedCount The number of elements that will be written. 175 * @return A streaming {@link RecordWriter}. 176 * @throws IOException If a problem occurred opening the underlying output. 177 */ 178 @Override /* ArrayRecordWritable */ 179 public RecordWriter writeArrayRecords(Object output, int expectedCount) throws IOException { 180 if (!(output instanceof OutputStream)) 181 throw new IOException("MsgPack streaming arrayRecordWriter requires an OutputStream"); 182 var os = (OutputStream) output; 183 var mpos = new MsgPackOutputStream(os); 184 mpos.startArray(expectedCount); 185 return new RecordWriter() { 186 private int written; 187 private boolean closed; 188 189 @Override public RecordWriter write(Object value) throws IOException { 190 if (closed) 191 throw new IllegalStateException("Array stream is closed."); 192 if (written >= expectedCount) 193 throw new IllegalStateException( 194 "Array stream is full; declared " + expectedCount + " elements, attempted to write more."); 195 try { 196 MsgPackSerializerSession.this.write(value, os); 197 } catch (SerializeException e) { 198 throw new IOException(e); 199 } 200 written++; 201 return this; 202 } 203 @Override public boolean isStreaming() { 204 return true; 205 } 206 @Override public void flush() throws IOException { 207 os.flush(); 208 } 209 @Override public void close() throws IOException { 210 if (closed) 211 return; 212 closed = true; 213 if (written != expectedCount) 214 throw new IOException( 215 "Array stream count mismatch: declared " + expectedCount + " but wrote " + written); 216 os.close(); 217 } 218 }; 219 } 220 221 /* 222 * Workhorse method. 223 * Determines the type of object, and then calls the appropriate type-specific serialization method. 224 */ 225 @SuppressWarnings({ 226 "java:S3776" // Cognitive complexity acceptable for serialization dispatch logic 227 }) 228 private MsgPackOutputStream writeAnything(MsgPackOutputStream out, Object o, ClassMeta<?> eType, String attrName, BeanPropertyMeta pMeta) throws SerializeException { 229 230 if (o == null) 231 return out.appendNull(); 232 233 if (eType == null) 234 eType = object(); 235 236 ClassMeta<?> aType = null; // The actual type 237 ClassMeta<?> sType = null; // The serialized type 238 239 aType = push2(attrName, o, eType); 240 241 // Handle recursion 242 if (aType == null) 243 return out.appendNull(); 244 245 // Handle Optional<X> 246 if (isOptional(aType)) { 247 o = getOptionalValue(o); 248 eType = getOptionalType(eType); 249 aType = getClassMetaForObject(o, object()); 250 } 251 252 sType = aType; 253 String typeName = getBeanTypeName(this, eType, aType, pMeta); 254 255 // Swap if necessary 256 var swap = aType.getSwap(this); 257 if (nn(swap)) { 258 o = swap(swap, o); 259 sType = swap.getSwapClassMeta(this); 260 261 // If the getSwapClass() method returns Object, we need to figure out 262 // the actual type now. 263 if (sType.isObject()) 264 sType = getClassMetaForObject(o); 265 } 266 267 // '\0' characters are considered null. 268 if (o == null || (sType.isChar() && ((Character)o).charValue() == 0)) 269 out.appendNull(); 270 else if (sType.isBean()) 271 writeBeanMap(out, toBeanMap(o), typeName); 272 else if (sType.isMap()) { 273 if (sType.isBeanMap()) 274 writeBeanMap(out, (BeanMap)o, typeName); 275 else 276 writeMap(out, (Map)o, eType); 277 } else if (sType.isCollection()) { 278 writeCollection(out, (Collection)o, eType); 279 } else if (sType.isByteArray()) { 280 var binaryFormat = getBinaryFormat(); 281 if (binaryFormat == BinaryFormat.NOT_SET) 282 out.appendBinary((byte[])o); 283 else 284 out.appendString(binaryFormat.format((byte[])o)); 285 } else if (sType.isArray()) { 286 writeCollection(out, toList(sType.inner(), o), eType); 287 } else if (sType.isBoolean()) { 288 out.appendBoolean((Boolean)o); 289 } else if (sType.isNumber()) { 290 out.appendNumber((Number)o); 291 } else if (sType.isUri() || (nn(pMeta) && pMeta.isUri())) { 292 out.appendString(resolveUri(o.toString())); 293 } else if (sType.isDate()) { 294 out.appendString(writeDate((Date)o, sType)); 295 } else if (sType.isCalendar()) { 296 out.appendString(writeCalendar(o, sType)); 297 } else if (sType.isTemporal()) { 298 out.appendString(writeTemporal((TemporalAccessor)o, sType)); 299 } else if (sType.isDuration()) { 300 appendDuration(out, (Duration)o); 301 } else if (sType.isPeriod()) { 302 out.appendString(writePeriod((Period)o)); 303 } else if (sType.isStreamable()) { 304 // MsgPack protocol requires array size in header (startArray(size)), so materialization is unavoidable. 305 writeCollection(out, toListFromStreamable(o, sType), eType); 306 } else if (sType.isReader()) { 307 pipe((Reader)o, out, SerializerSession::handleThrown); 308 } else if (sType.isInputStream()) { 309 pipe((InputStream)o, out, SerializerSession::handleThrown); 310 } else 311 out.appendString(toString(o)); 312 313 pop(); 314 return out; 315 } 316 317 private void writeBeanMap(MsgPackOutputStream out, BeanMap<?> m, String typeName) throws SerializeException { 318 319 Predicate<Object> checkNull = x -> isKeepNullProperties() || nn(x); 320 321 var values = new ArrayList<BeanPropertyValue>(); 322 323 if (nn(typeName)) { 324 BeanPropertyMeta pm = m.getMeta().getTypeProperty(); 325 values.add(new BeanPropertyValue(pm, pm.getName(), typeName, null)); 326 } 327 328 m.forEachValue(checkNull, (pMeta, key, value, thrown) -> { 329 if (nn(thrown)) { 330 onBeanGetterException(pMeta, thrown); 331 return; 332 } 333 var p = new BeanPropertyValue(pMeta, key, value, null); 334 335 if ((! isKeepNullProperties()) && willRecurse(p)) { 336 return; // Must handle the case where recursion occurs and property is not serialized. 337 } 338 339 values.add(p); 340 }); 341 342 out.startMap(values.size()); 343 344 values.forEach(x -> { 345 BeanPropertyMeta pMeta = x.getMeta(); 346 if (pMeta.canRead()) { 347 ClassMeta<?> cMeta = (ClassMeta<?>) x.getBeanInfo(); 348 String key = x.getName(); 349 Object value = x.getValue(); 350 writeAnything(out, key, null, null, null); 351 writeAnything(out, value, cMeta, key, pMeta); 352 } 353 }); 354 } 355 356 private void writeCollection(MsgPackOutputStream out, Collection c, ClassMeta<?> type) throws SerializeException { 357 var elementType = type.getElementType(); 358 List<Object> l = listOfSize(c.size()); 359 c = sort(c); 360 l.addAll(c); 361 out.startArray(l.size()); 362 l.forEach(x -> writeAnything(out, x, elementType, "<iterator>", null)); 363 } 364 365 private void writeMap(MsgPackOutputStream out, Map m, ClassMeta<?> type) throws SerializeException { 366 367 var keyType = type.getKeyType(); 368 var valueType = type.getValueType(); 369 370 m = sort(m); 371 372 // The map size may change as we're iterating over it, so 373 // grab a snapshot of the entries in a separate list. 374 List<SimpleMapEntry> entries = listOfSize(m.size()); 375 m.forEach((k, v) -> entries.add(new SimpleMapEntry(k, v))); 376 377 out.startMap(entries.size()); 378 379 entries.forEach(x -> { 380 Object value = x.value(); 381 Object key = generalize(x.key(), keyType); 382 writeAnything(out, key, keyType, null, null); 383 writeAnything(out, value, valueType, null, null); 384 }); 385 } 386 387 private boolean willRecurse(BeanPropertyValue v) throws SerializeException { 388 var aType = push2(v.getName(), v.getValue(), (ClassMeta<?>) v.getBeanInfo()); 389 if (nn(aType)) 390 pop(); 391 return aType == null; 392 } 393 394 @Override /* Overridden from SerializerSession */ 395 protected void doWrite(SerializerPipe out, Object o) throws IOException, SerializeException { 396 writeAnything(getMsgPackOutputStream(out), o, getExpectedRootType(o), "root", null); 397 } 398 399 @Override 400 public boolean isAddBeanTypes() { return super.isAddBeanTypes(); } 401 402 private void appendDuration(MsgPackOutputStream out, Duration value) { 403 var f = getDurationFormat(); 404 var s = writeDuration(value); 405 if (f == DurationFormat.NANOS || f == DurationFormat.MILLIS) 406 out.appendNumber(Long.parseLong(s)); 407 else if (f == DurationFormat.SECONDS) 408 out.appendNumber(Double.parseDouble(s)); 409 else 410 out.appendString(s); 411 } 412}