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 019 020import java.io.*; 021import java.math.*; 022 023import org.apache.juneau.marshall.stream.*; 024import static org.apache.juneau.commons.utils.Shorts.*; 025 026/** 027 * Reference implementation of the public {@link TokenWriter} surface for the CBOR format 028 * (RFC 8949). 029 * 030 * <p> 031 * Wraps a {@link CborOutputStream} and emits **indefinite-length** containers 032 * (<c>0xBF</c> for maps, <c>0x9F</c> for arrays, terminated by <c>0xFF</c> BREAK) so that the 033 * caller doesn't have to know element counts up front — a natural fit for streaming 034 * generation. 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 CBOR major-type-2 byte strings via 039 * {@link CborOutputStream#appendBinary(byte[])}, not to a base64 string fallback. 040 * <li>The writer is purely structural; object swaps and {@code @Schema} annotations are NOT 041 * applied at the token layer (see {@link TokenWriter#object(Object)} for the bridge that 042 * does apply them). 043 * </ul> 044 */ 045@SuppressWarnings({ 046 "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. 047}) 048public class CborTokenWriter implements TokenWriter { 049 050 /** 051 * Cursor-level settings honored by {@link CborTokenWriter}. 052 * 053 * @param walk The {@link org.apache.juneau.marshall.stream.PojoWalker.Options walk options} used by 054 * {@link #object(Object)} when walking a value graph. 055 */ 056 public record Settings(PojoWalker.Options walk) { 057 058 /** Default canonical setting: default walk options. */ 059 public static final Settings DEFAULT = new Settings(PojoWalker.Options.DEFAULT); 060 } 061 062 private final CborOutputStream out; 063 private final OutputStream raw; 064 private final Closeable owned; 065 private final Settings settings; 066 067 // Container stack: 0 = array, 1 = map; tracked depth lets us validate end-kind matches. 068 private boolean[] stackIsMap = new boolean[16]; 069 private int depth; 070 // Map state: track whether we're emitting a key (true) or a value (false) at the current 071 // map level. 072 private boolean[] stackAwaitingKey = new boolean[16]; 073 074 private boolean closed; 075 076 /** 077 * Constructor with default settings. 078 * 079 * @param out The {@link OutputStream} to emit CBOR to. Must not be <jk>null</jk>. 080 */ 081 public CborTokenWriter(OutputStream out) { 082 this(out, null, Settings.DEFAULT); 083 } 084 085 /** 086 * Constructor with explicit settings. 087 * 088 * @param out The {@link OutputStream} to emit CBOR to. Must not be <jk>null</jk>. 089 * @param settings The settings. Must not be <jk>null</jk>. 090 */ 091 public CborTokenWriter(OutputStream out, Settings settings) { 092 this(out, null, settings); 093 } 094 095 private CborTokenWriter(OutputStream out, Closeable owned, Settings settings) { 096 reqnn("out", out); 097 reqnn("settings", settings); 098 this.raw = out; 099 this.out = new CborOutputStream(out); 100 this.owned = owned; 101 this.settings = settings; 102 } 103 104 @Override /* TokenWriter */ 105 public TokenWriter startObject() throws IOException { 106 assertOpen(); 107 preValueWrite(); 108 // 0xBF = indefinite-length map start. 109 raw.write(0xBF); 110 pushContainer(true); 111 return this; 112 } 113 114 @Override /* TokenWriter */ 115 public TokenWriter endObject() throws IOException { 116 assertOpen(); 117 popContainer(true); 118 // 0xFF = BREAK (closes both indefinite arrays and maps). 119 raw.write(0xFF); 120 afterValue(); 121 return this; 122 } 123 124 @Override /* TokenWriter */ 125 public TokenWriter startArray() throws IOException { 126 assertOpen(); 127 preValueWrite(); 128 // 0x9F = indefinite-length array start. 129 raw.write(0x9F); 130 pushContainer(false); 131 return this; 132 } 133 134 @Override /* TokenWriter */ 135 public TokenWriter endArray() throws IOException { 136 assertOpen(); 137 popContainer(false); 138 raw.write(0xFF); 139 afterValue(); 140 return this; 141 } 142 143 @Override /* TokenWriter */ 144 public TokenWriter fieldName(String name) throws IOException { 145 assertOpen(); 146 reqnn("name", name); 147 if (depth == 0 || !stackIsMap[depth - 1]) 148 throw new IllegalStateException("field called outside an object"); 149 if (!stackAwaitingKey[depth - 1]) 150 throw new IllegalStateException("field called twice without an intervening value"); 151 out.appendString(name); 152 stackAwaitingKey[depth - 1] = false; 153 return this; 154 } 155 156 @Override /* TokenWriter */ 157 public TokenWriter string(String value) throws IOException { 158 assertOpen(); 159 if (value == null) 160 return nil(); 161 preValueWrite(); 162 out.appendString(value); 163 afterValue(); 164 return this; 165 } 166 167 @Override /* TokenWriter */ 168 public TokenWriter number(Number value) throws IOException { 169 assertOpen(); 170 if (value == null) 171 return nil(); 172 preValueWrite(); 173 out.appendNumber(value); 174 afterValue(); 175 return this; 176 } 177 178 @Override /* TokenWriter */ 179 public TokenWriter number(long value) throws IOException { 180 assertOpen(); 181 preValueWrite(); 182 out.appendLong(value); 183 afterValue(); 184 return this; 185 } 186 187 @Override /* TokenWriter */ 188 public TokenWriter number(double value) throws IOException { 189 assertOpen(); 190 preValueWrite(); 191 out.appendDouble(value); 192 afterValue(); 193 return this; 194 } 195 196 @Override /* TokenWriter */ 197 public TokenWriter number(BigDecimal value) throws IOException { 198 assertOpen(); 199 if (value == null) 200 return nil(); 201 preValueWrite(); 202 out.appendDouble(value.doubleValue()); 203 afterValue(); 204 return this; 205 } 206 207 @Override /* TokenWriter */ 208 public TokenWriter number(BigInteger value) throws IOException { 209 assertOpen(); 210 if (value == null) 211 return nil(); 212 preValueWrite(); 213 // Delegates to appendNumber so values outside the signed-long range are emitted losslessly 214 // (native CBOR integer up to ±2^64, decimal string beyond) rather than throwing on overflow. 215 out.appendNumber(value); 216 afterValue(); 217 return this; 218 } 219 220 @Override /* TokenWriter */ 221 public TokenWriter bool(boolean value) throws IOException { 222 assertOpen(); 223 preValueWrite(); 224 out.appendBoolean(value); 225 afterValue(); 226 return this; 227 } 228 229 @Override /* TokenWriter */ 230 public TokenWriter nil() throws IOException { 231 assertOpen(); 232 preValueWrite(); 233 out.appendNull(); 234 afterValue(); 235 return this; 236 } 237 238 @Override /* TokenWriter */ 239 public TokenWriter binary(byte[] value) throws IOException { 240 assertOpen(); 241 if (value == null) 242 return nil(); 243 preValueWrite(); 244 out.appendBinary(value); 245 afterValue(); 246 return this; 247 } 248 249 @Override /* TokenWriter */ 250 public CborTokenWriter writeTag(long tagNumber) throws IOException { 251 assertOpen(); 252 // A tag is a prefix on the next value emit; it does NOT consume a map-key/value or 253 // array-element slot. Skip preValueWrite()/afterValue() — the wrapped value emit that 254 // follows owns the state transition. 255 out.writeTag(tagNumber); 256 return this; 257 } 258 259 @Override /* TokenWriter */ 260 public CborTokenWriter writeSimple(int value) throws IOException { 261 assertOpen(); 262 preValueWrite(); 263 out.writeSimple(value); 264 afterValue(); 265 return this; 266 } 267 268 @Override /* TokenWriter */ 269 public TokenWriter object(Object value) throws IOException { 270 assertOpen(); 271 PojoWalker.walk(this, value, settings.walk); 272 return this; 273 } 274 275 @Override /* TokenWriter */ 276 public boolean isStreaming() { 277 return true; 278 } 279 280 @Override /* TokenWriter */ 281 public void flush() throws IOException { 282 raw.flush(); 283 } 284 285 @Override /* TokenWriter */ 286 public void close() throws IOException { 287 if (closed) 288 return; 289 closed = true; 290 try { 291 raw.flush(); 292 } finally { 293 if (owned != null) 294 owned.close(); 295 } 296 } 297 298 // ================================================================================= 299 // Helpers 300 // ================================================================================= 301 302 /** 303 * Throws an {@link IOException} if this writer has been {@link #close() closed}. 304 * 305 * @throws IOException If this writer is closed. 306 */ 307 private void assertOpen() throws IOException { 308 if (closed) 309 throw new IOException("Token writer is closed."); 310 } 311 312 private void preValueWrite() { 313 if (depth == 0) 314 return; 315 if (stackIsMap[depth - 1] && stackAwaitingKey[depth - 1]) 316 throw new IllegalStateException( 317 "Value emitted at map-key position without preceding field(...)."); 318 } 319 320 private void afterValue() { 321 if (depth == 0) 322 return; 323 if (stackIsMap[depth - 1]) 324 stackAwaitingKey[depth - 1] = true; // next emit inside this map should be a key 325 } 326 327 @SuppressWarnings({ 328 "javabugs:S6466" // The stack arrays start at 16 and double whenever depth reaches their length, so depth always indexes a live slot. 329 }) 330 private void pushContainer(boolean isMap) { 331 if (depth == stackIsMap.length) { 332 var n = depth * 2; 333 var nm = new boolean[n]; 334 var nk = new boolean[n]; 335 System.arraycopy(stackIsMap, 0, nm, 0, depth); 336 System.arraycopy(stackAwaitingKey, 0, nk, 0, depth); 337 stackIsMap = nm; 338 stackAwaitingKey = nk; 339 } 340 stackIsMap[depth] = isMap; 341 stackAwaitingKey[depth] = isMap; // map starts awaiting a key 342 depth++; 343 } 344 345 private void popContainer(boolean expectMap) { 346 if (depth == 0) 347 throw new IllegalStateException("end-container called with no matching start-container"); 348 var actualIsMap = stackIsMap[depth - 1]; 349 if (actualIsMap != expectMap) 350 throw new IllegalStateException( 351 "end-container kind mismatch: expected " + (expectMap ? "object" : "array") + 352 ", got " + (actualIsMap ? "object" : "array")); 353 depth--; 354 } 355}