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.jsonl; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.io.*; 022import java.math.*; 023import java.nio.charset.*; 024 025import org.apache.juneau.marshall.json.*; 026import org.apache.juneau.marshall.stream.*; 027 028/** 029 * Reference implementation of the public {@link TokenWriter} surface for the JSONL / NDJSON 030 * format. 031 * 032 * <p> 033 * Wraps a {@link JsonTokenWriter} and appends a newline after every top-level value emit. The 034 * "top-level value" boundary is detected by tracking when an emit returns the cursor to 035 * {@code depth == 0}. 036 * 037 * <h5 class='section'>Notes:</h5><ul> 038 * <li class='warn'>This class is not thread safe. 039 * </ul> 040 */ 041@SuppressWarnings({ 042 "resource" // The writer's underlying Writer/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 JsonlTokenWriter implements TokenWriter { 045 046 private final JsonTokenWriter delegate; 047 private final Writer out; 048 private final Closeable owned; 049 private int depth; 050 private boolean closed; 051 052 /** 053 * Constructor. 054 * 055 * @param out The writer to emit JSONL to. Must not be <jk>null</jk>. 056 * @param settings The output-formatting settings. Must not be <jk>null</jk>. 057 */ 058 public JsonlTokenWriter(Writer out, JsonTokenWriter.Settings settings) { 059 this(out, settings, null); 060 } 061 062 /** 063 * Internal constructor that records an underlying {@link Closeable} the writer should close on 064 * shutdown (e.g. a file stream opened by {@link #forOutput(Object, JsonTokenWriter.Settings)}). 065 * 066 * @param out The writer to emit JSONL to. Must not be <jk>null</jk>. 067 * @param settings The output-formatting settings. Must not be <jk>null</jk>. 068 * @param owned A resource owned by this writer that should be closed on close, or <jk>null</jk> (no extra 069 * resource is closed). 070 */ 071 JsonlTokenWriter(Writer out, JsonTokenWriter.Settings settings, Closeable owned) { 072 this.delegate = new JsonTokenWriter(out, settings); 073 this.out = out; 074 this.owned = owned; 075 } 076 077 /** 078 * Internal factory that mirrors {@link JsonTokenWriter#forOutput(Object, JsonTokenWriter.Settings)} 079 * so the per-format {@code tokenWriter(...)} factory can hand in any of the supported output 080 * types ({@link Writer}, {@link OutputStream}). 081 * 082 * @param output The output object. 083 * @param settings The output-formatting settings. 084 * @return A new {@link JsonlTokenWriter} writing to the resolved underlying writer. 085 * @throws IOException If the output type is not supported or could not be opened. 086 */ 087 public static JsonlTokenWriter forOutput(Object output, JsonTokenWriter.Settings settings) throws IOException { 088 if (output == null) 089 throw new IOException("Output cannot be null."); 090 if (output instanceof Writer output2) 091 return new JsonlTokenWriter(output2, settings); 092 if (output instanceof OutputStream output2) 093 return new JsonlTokenWriter(new OutputStreamWriter(output2, StandardCharsets.UTF_8), settings); 094 throw ioex("Cannot convert object of type %s to a Writer.", output.getClass().getName()); 095 } 096 097 @Override public TokenWriter startObject() throws IOException { assertOpen(); delegate.startObject(); depth++; return this; } 098 @Override public TokenWriter endObject() throws IOException { assertOpen(); delegate.endObject(); return endTopLevel(); } 099 @Override public TokenWriter startArray() throws IOException { assertOpen(); delegate.startArray(); depth++; return this; } 100 @Override public TokenWriter endArray() throws IOException { assertOpen(); delegate.endArray(); return endTopLevel(); } 101 @Override public TokenWriter fieldName(String name) throws IOException { assertOpen(); delegate.fieldName(name); return this; } 102 @Override public TokenWriter string(String value) throws IOException { assertOpen(); delegate.string(value); return scalarTopLevel(); } 103 @Override public TokenWriter number(Number value) throws IOException { assertOpen(); delegate.number(value); return scalarTopLevel(); } 104 @Override public TokenWriter number(long value) throws IOException { assertOpen(); delegate.number(value); return scalarTopLevel(); } 105 @Override public TokenWriter number(double value) throws IOException { assertOpen(); delegate.number(value); return scalarTopLevel(); } 106 @Override public TokenWriter number(BigDecimal value) throws IOException { assertOpen(); delegate.number(value); return scalarTopLevel(); } 107 @Override public TokenWriter number(BigInteger value) throws IOException { assertOpen(); delegate.number(value); return scalarTopLevel(); } 108 @Override public TokenWriter bool(boolean value) throws IOException { assertOpen(); delegate.bool(value); return scalarTopLevel(); } 109 @Override public TokenWriter nil() throws IOException { assertOpen(); delegate.nil(); return scalarTopLevel(); } 110 @Override public TokenWriter binary(byte[] value) throws IOException { assertOpen(); delegate.binary(value); return scalarTopLevel(); } 111 112 @Override 113 public TokenWriter object(Object value) throws IOException { 114 assertOpen(); 115 // Walk through THIS writer's structural methods so the depth tracking + trailing-newline 116 // logic fires correctly. PojoWalker honors the WalkOptions configured on the underlying 117 // delegate's Settings. 118 PojoWalker.walk(this, value, delegate.settingsAccess().walk()); 119 return this; 120 } 121 122 @Override public boolean isStreaming() { return true; } 123 @Override public void flush() throws IOException { out.flush(); } 124 125 @Override 126 public void close() throws IOException { 127 if (closed) 128 return; 129 closed = true; 130 try { 131 delegate.close(); 132 } finally { 133 if (owned != null) 134 owned.close(); 135 } 136 } 137 138 /** 139 * Throws an {@link IOException} if this writer has been {@link #close() closed}. 140 * 141 * @throws IOException If this writer is closed. 142 */ 143 private void assertOpen() throws IOException { 144 if (closed) 145 throw new IOException("Token writer is closed."); 146 } 147 148 private TokenWriter endTopLevel() throws IOException { 149 depth--; 150 if (depth == 0) 151 out.write('\n'); 152 return this; 153 } 154 155 private TokenWriter scalarTopLevel() throws IOException { 156 if (depth == 0) 157 out.write('\n'); 158 return this; 159 } 160}