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}