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.ObjectUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021 022import java.io.*; 023import java.math.*; 024 025import org.apache.juneau.marshall.stream.*; 026 027/** 028 * Reference implementation of the public {@link TokenWriter} surface for the MessagePack format. 029 * 030 * <p> 031 * MessagePack containers are length-prefixed (no indefinite-length encoding); the writer therefore 032 * <b>buffers</b> each open container's body bytes until {@link #endObject()} / {@link #endArray()} 033 * is called, then writes the length-prefix header followed by the buffered bytes. Memory cost 034 * is O(largest open container). 035 * 036 * <h5 class='section'>Notes:</h5><ul> 037 * <li class='warn'>This class is not thread safe. 038 * <li>{@code byte[]} values dispatch to native MsgPack {@code bin}, not to a base64 string fallback. 039 * </ul> 040 */ 041@SuppressWarnings({ 042 "resource" // The writer's underlying OutputStream is owned by the caller via try-with-resources on the writer itself; Eclipse JDT flags the inner stream as unclosed but that's by design. 043}) 044public class MsgPackTokenWriter implements TokenWriter { 045 046 /** 047 * Cursor-level settings honored by {@link MsgPackTokenWriter}. 048 * 049 * @param walk The {@link PojoWalker.Options walk options} used by {@link #object(Object)}. 050 */ 051 @SuppressWarnings({ 052 "javadoc" // Record components are documented in the enclosing Javadoc; per-component tags are omitted. 053 }) 054 public record Settings(PojoWalker.Options walk) { 055 056 /** Default canonical setting: default walk options. */ 057 public static final Settings DEFAULT = new Settings(PojoWalker.Options.DEFAULT); 058 } 059 060 private final OutputStream finalOut; 061 private final Closeable owned; 062 private final Settings settings; 063 064 // Per-level buffers. The active write target is the top of this stack; if depth==0 we write 065 // directly to finalOut. 066 private final java.util.ArrayDeque<ByteArrayOutputStream> buffers = new java.util.ArrayDeque<>(); 067 // Per-level container kind: true=map, false=array. 068 private final java.util.ArrayDeque<Boolean> isMapStack = new java.util.ArrayDeque<>(); 069 // Per-level element-count (for arrays = number of values emitted; for maps = number of pairs). 070 // For maps, incremented once per field() (= once per pair). 071 private final java.util.ArrayDeque<Integer> elementCount = new java.util.ArrayDeque<>(); 072 // Per-level: for maps, true when the next emit should be a field (key), false after the 073 // field when the next emit should be the value. 074 private final java.util.ArrayDeque<Boolean> awaitingKey = new java.util.ArrayDeque<>(); 075 076 private boolean closed; 077 078 /** 079 * Constructor with default settings. 080 * 081 * @param out The {@link OutputStream} to emit MsgPack to. Must not be <jk>null</jk>. 082 */ 083 public MsgPackTokenWriter(OutputStream out) { 084 this(out, null, Settings.DEFAULT); 085 } 086 087 /** 088 * Constructor with explicit settings. 089 * 090 * @param out The {@link OutputStream} to emit MsgPack to. Must not be <jk>null</jk>. 091 * @param settings The settings. Must not be <jk>null</jk>. 092 */ 093 public MsgPackTokenWriter(OutputStream out, Settings settings) { 094 this(out, null, settings); 095 } 096 097 private MsgPackTokenWriter(OutputStream out, Closeable owned, Settings settings) { 098 reqnn("out", out); 099 reqnn("settings", settings); 100 this.finalOut = out; 101 this.owned = owned; 102 this.settings = settings; 103 } 104 105 /** The active write target for child bytes (top buffer or finalOut). */ 106 private OutputStream activeOut() { 107 return buffers.isEmpty() ? finalOut : buffers.peek(); 108 } 109 110 @Override /* TokenWriter */ 111 public TokenWriter startObject() throws IOException { 112 assertOpen(); 113 // Push a new buffer; child writes go there until endObject. 114 buffers.push(new ByteArrayOutputStream()); 115 isMapStack.push(true); 116 elementCount.push(0); 117 awaitingKey.push(true); 118 return this; 119 } 120 121 @Override /* TokenWriter */ 122 public TokenWriter endObject() throws IOException { 123 assertOpen(); 124 if (buffers.isEmpty() || !isTrue(isMapStack.peek())) 125 throw new IllegalStateException("endObject called with no matching startObject"); 126 var body = buffers.pop(); 127 var count = elementCount.pop(); 128 isMapStack.pop(); 129 awaitingKey.pop(); 130 // Write the map header to whatever is now the active stream, then the buffered body. 131 var header = new MsgPackOutputStream(activeOut()); 132 header.startMap(count); 133 body.writeTo(activeOut()); 134 afterValue(); 135 return this; 136 } 137 138 @Override /* TokenWriter */ 139 public TokenWriter startArray() throws IOException { 140 assertOpen(); 141 buffers.push(new ByteArrayOutputStream()); 142 isMapStack.push(false); 143 elementCount.push(0); 144 awaitingKey.push(false); 145 return this; 146 } 147 148 @Override /* TokenWriter */ 149 public TokenWriter endArray() throws IOException { 150 assertOpen(); 151 if (buffers.isEmpty() || isTrue(isMapStack.peek())) 152 throw new IllegalStateException("endArray called with no matching startArray"); 153 var body = buffers.pop(); 154 var count = elementCount.pop(); 155 isMapStack.pop(); 156 awaitingKey.pop(); 157 var header = new MsgPackOutputStream(activeOut()); 158 header.startArray(count); 159 body.writeTo(activeOut()); 160 afterValue(); 161 return this; 162 } 163 164 @Override /* TokenWriter */ 165 public TokenWriter fieldName(String name) throws IOException { 166 assertOpen(); 167 reqnn("name", name); 168 if (isMapStack.isEmpty() || !isTrue(isMapStack.peek())) 169 throw new IllegalStateException("field called outside an object"); 170 if (!isTrue(awaitingKey.peek())) 171 throw new IllegalStateException("field called twice without an intervening value"); 172 new MsgPackOutputStream(activeOut()).appendString(name); 173 // Replace top of awaitingKey with false (we just wrote the key; expect a value next). 174 awaitingKey.pop(); 175 awaitingKey.push(false); 176 // Increment pair count once (per pair) — happens at the field, not the value. 177 var c = elementCount.pop(); 178 elementCount.push(c + 1); 179 return this; 180 } 181 182 @Override /* TokenWriter */ 183 public TokenWriter string(String value) throws IOException { 184 assertOpen(); 185 if (value == null) 186 return nil(); 187 preValueCheck(); 188 new MsgPackOutputStream(activeOut()).appendString(value); 189 afterValue(); 190 return this; 191 } 192 193 @Override /* TokenWriter */ 194 public TokenWriter number(Number value) throws IOException { 195 assertOpen(); 196 if (value == null) 197 return nil(); 198 preValueCheck(); 199 new MsgPackOutputStream(activeOut()).appendNumber(value); 200 afterValue(); 201 return this; 202 } 203 204 @Override /* TokenWriter */ 205 public TokenWriter number(long value) throws IOException { 206 assertOpen(); 207 preValueCheck(); 208 new MsgPackOutputStream(activeOut()).appendLong(value); 209 afterValue(); 210 return this; 211 } 212 213 @Override /* TokenWriter */ 214 public TokenWriter number(double value) throws IOException { 215 assertOpen(); 216 preValueCheck(); 217 new MsgPackOutputStream(activeOut()).appendDouble(value); 218 afterValue(); 219 return this; 220 } 221 222 @Override /* TokenWriter */ 223 public TokenWriter number(BigDecimal value) throws IOException { 224 assertOpen(); 225 if (value == null) 226 return nil(); 227 preValueCheck(); 228 new MsgPackOutputStream(activeOut()).appendDouble(value.doubleValue()); 229 afterValue(); 230 return this; 231 } 232 233 @Override /* TokenWriter */ 234 public TokenWriter number(BigInteger value) throws IOException { 235 assertOpen(); 236 if (value == null) 237 return nil(); 238 preValueCheck(); 239 // Align with the databind serializer path (G4): long-range -> INT64, [2^63,2^64-1] -> UINT64, 240 // otherwise throw — rather than the previous longValueExact() which threw on the representable 241 // [2^63,2^64-1] range. 242 new MsgPackOutputStream(activeOut()).appendBigInteger(value); 243 afterValue(); 244 return this; 245 } 246 247 @Override /* TokenWriter */ 248 public TokenWriter bool(boolean value) throws IOException { 249 assertOpen(); 250 preValueCheck(); 251 new MsgPackOutputStream(activeOut()).appendBoolean(value); 252 afterValue(); 253 return this; 254 } 255 256 @Override /* TokenWriter */ 257 public TokenWriter nil() throws IOException { 258 assertOpen(); 259 preValueCheck(); 260 new MsgPackOutputStream(activeOut()).appendNull(); 261 afterValue(); 262 return this; 263 } 264 265 @Override /* TokenWriter */ 266 public TokenWriter binary(byte[] value) throws IOException { 267 assertOpen(); 268 if (value == null) 269 return nil(); 270 preValueCheck(); 271 new MsgPackOutputStream(activeOut()).appendBinary(value); 272 afterValue(); 273 return this; 274 } 275 276 @Override /* TokenWriter */ 277 public MsgPackTokenWriter writeExt(int type, byte[] payload) throws IOException { 278 assertOpen(); 279 if (payload == null) 280 throw new IllegalArgumentException("ext payload must not be null"); 281 preValueCheck(); 282 new MsgPackOutputStream(activeOut()).writeExt(type, payload); 283 afterValue(); 284 return this; 285 } 286 287 @Override /* TokenWriter */ 288 public TokenWriter object(Object value) throws IOException { 289 assertOpen(); 290 PojoWalker.walk(this, value, settings.walk); 291 return this; 292 } 293 294 @Override /* TokenWriter */ 295 public boolean isStreaming() { return true; } 296 297 @Override /* TokenWriter */ 298 public void flush() throws IOException { finalOut.flush(); } 299 300 @Override /* TokenWriter */ 301 public void close() throws IOException { 302 if (closed) 303 return; 304 closed = true; 305 try { 306 finalOut.flush(); 307 } finally { 308 if (owned != null) 309 owned.close(); 310 } 311 } 312 313 /** 314 * Throws an {@link IOException} if this writer has been {@link #close() closed}. 315 * 316 * @throws IOException If this writer is closed. 317 */ 318 private void assertOpen() throws IOException { 319 if (closed) 320 throw new IOException("Token writer is closed."); 321 } 322 323 private void preValueCheck() { 324 // At map-key position, value emits are illegal — caller must call field() first. 325 if (!isMapStack.isEmpty() && isTrue(isMapStack.peek()) && isTrue(awaitingKey.peek())) 326 throw new IllegalStateException( 327 "Value emitted at map-key position without preceding field(...)."); 328 } 329 330 private void afterValue() { 331 if (isMapStack.isEmpty()) 332 return; 333 // After emitting a value: 334 // - In a map, flip awaitingKey back to true (next emit is the next pair's key). 335 // - In an array, increment element count. 336 if (isTrue(isMapStack.peek())) { 337 awaitingKey.pop(); 338 awaitingKey.push(true); 339 } else { 340 var c = elementCount.pop(); 341 elementCount.push(c + 1); 342 } 343 } 344}