content: /*
   * Copyright 2021-2024 Aklivity Inc
   *
   * Licensed under the Aklivity Community License (the "License"); you may not use
   * this file except in compliance with the License.  You may obtain a copy of the
   * License at
   *
   *   https://www.aklivity.io/aklivity-community-license/
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   * WARRANTIES OF ANY KIND, either express or implied.  See the License for the
   * specific language governing permissions and limitations under the License.
   */
  package org.apache.avro.io;
  
  import java.io.IOException;
  import java.io.OutputStream;
  import java.nio.ByteBuffer;
  import java.nio.charset.StandardCharsets;
  import java.util.BitSet;
  import java.util.Objects;
  
  import org.apache.avro.AvroTypeException;
  import org.apache.avro.Schema;
  import org.apache.avro.io.parsing.JsonGrammarGenerator;
  import org.apache.avro.io.parsing.Parser;
  import org.apache.avro.io.parsing.Symbol;
  import org.apache.avro.util.Utf8;
  
  import com.fasterxml.jackson.core.JsonEncoding;
  import com.fasterxml.jackson.core.JsonFactory;
  import com.fasterxml.jackson.core.JsonGenerator;
  import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
  import com.fasterxml.jackson.core.util.MinimalPrettyPrinter;
  
  /**
   * An {@link Encoder} for Avro's JSON data encoding.
   * </p>
   * Construct using {@link EncoderFactory}.
   * </p>
   * JsonEncoder buffers output, and data may not appear on the output until
   * {@link Encoder#flush()} is called.
   * </p>
   * JsonEncoder is not thread-safe.
   */
  public class JsonEncoder extends ParsingEncoder implements Parser.ActionHandler
  {
      private static final String LINE_SEPARATOR = System.getProperty("line.separator");
      final Parser parser;
      private boolean includeNamespace = true;
      JsonGenerator out;
  
      /**
       * Has anything been written into the collections?
       */
      protected BitSet isEmpty = new BitSet();
  
      JsonEncoder(
          Schema sc,
          OutputStream out) throws IOException
      {
          this(sc, getJsonGenerator(out, false));
      }
  
      JsonEncoder(
          Schema sc,
          OutputStream out,
          boolean pretty) throws IOException
      {
          this(sc, getJsonGenerator(out, pretty));
      }
  
      JsonEncoder(Schema sc, JsonGenerator out) throws IOException
      {
          configure(out);
          this.parser = new Parser(new JsonGrammarGenerator().generate(sc), this);
      }
  
      @Override
      public void flush() throws IOException
      {
          parser.processImplicitActions();
          if (out != null)
          {
              out.flush();
          }
      }
  
      // by default, one object per line.
      // with pretty option use default pretty printer with root line separator.
      private static JsonGenerator getJsonGenerator(
          OutputStream out,
          boolean pretty) throws IOException
      {
          Objects.requireNonNull(out, "OutputStream cannot be null");
          JsonGenerator g = new JsonFactory().createGenerator(out, JsonEncoding.UTF8);
          if (pretty)
          {
              DefaultPrettyPrinter pp = new DefaultPrettyPrinter()
              {
                  @Override
                  public void writeRootValueSeparator(JsonGenerator jg) throws IOException
                  {
                      jg.writeRaw(LINE_SEPARATOR);
                  }
              };
              g.setPrettyPrinter(pp);
          }
          else
          {
              MinimalPrettyPrinter pp = new MinimalPrettyPrinter();
              pp.setRootValueSeparator(LINE_SEPARATOR);
              g.setPrettyPrinter(pp);
          }
          return g;
      }
  
      public boolean isIncludeNamespace()
      {
          return includeNamespace;
      }
  
      public void setIncludeNamespace(
          final boolean includeNamespace)
      {
          this.includeNamespace = includeNamespace;
      }
  
      /**
       * Reconfigures this JsonEncoder to use the output stream provided.
       * <p/>
       * If the OutputStream provided is null, a NullPointerException is thrown.
       * <p/>
       * Otherwise, this JsonEncoder will flush its current output and then
       * reconfigure its output to use a default UTF8 JsonGenerator that writes to the
       * provided OutputStream.
       *
       * @param out The OutputStream to direct output to. Cannot be null.
       * @return this JsonEncoder
       * @throws IOException
       * @throws NullPointerException if {@code out} is {@code null}
       */
      public JsonEncoder configure(
          OutputStream out) throws IOException
      {
          this.configure(getJsonGenerator(out, false));
          return this;
      }
  
      /**
       * Reconfigures this JsonEncoder to output to the JsonGenerator provided.
       * <p/>
       * If the JsonGenerator provided is null, a NullPointerException is thrown.
       * <p/>
       * Otherwise, this JsonEncoder will flush its current output and then
       * reconfigure its output to use the provided JsonGenerator.
       *
       * @param generator The JsonGenerator to direct output to. Cannot be null.
       * @return this JsonEncoder
       * @throws IOException
       * @throws NullPointerException if {@code generator} is {@code null}
       */
      private JsonEncoder configure(
          JsonGenerator generator) throws IOException
      {
          Objects.requireNonNull(generator, "JsonGenerator cannot be null");
          if (null != parser)
          {
              flush();
          }
          this.out = generator;
          return this;
      }
  
      @Override
      public void writeNull() throws IOException
      {
          parser.advance(Symbol.NULL);
          out.writeNull();
      }
  
      @Override
      public void writeBoolean(
          boolean b) throws IOException
      {
          parser.advance(Symbol.BOOLEAN);
          out.writeBoolean(b);
      }
  
      @Override
      public void writeInt(
          int n) throws IOException
      {
          parser.advance(Symbol.INT);
          out.writeNumber(n);
      }
  
      @Override
      public void writeLong(
          long n) throws IOException
      {
          parser.advance(Symbol.LONG);
          out.writeNumber(n);
      }
  
      @Override
      public void writeFloat(
          float f) throws IOException
      {
          parser.advance(Symbol.FLOAT);
          out.writeNumber(f);
      }
  
      @Override
      public void writeDouble(
          double d) throws IOException
      {
          parser.advance(Symbol.DOUBLE);
          out.writeNumber(d);
      }
  
      @Override
      public void writeString(
          Utf8 utf8) throws IOException
      {
          writeString(utf8.toString());
      }
  
      @Override
      public void writeString(
          String str) throws IOException
      {
          parser.advance(Symbol.STRING);
          if (parser.topSymbol() == Symbol.MAP_KEY_MARKER)
          {
              parser.advance(Symbol.MAP_KEY_MARKER);
              out.writeFieldName(str);
          }
          else
          {
              out.writeString(str);
          }
      }
  
      @Override
      public void writeBytes(ByteBuffer bytes) throws IOException
      {
          if (bytes.hasArray())
          {
              writeBytes(bytes.array(), bytes.position(), bytes.remaining());
          }
          else
          {
              byte[] b = new byte[bytes.remaining()];
              bytes.duplicate().get(b);
              writeBytes(b);
          }
      }
  
      @Override
      public void writeBytes(
          byte[] bytes,
          int start,
          int len) throws IOException
      {
          parser.advance(Symbol.BYTES);
          writeByteArray(bytes, start, len);
      }
  
      private void writeByteArray(
          byte[] bytes,
          int start,
          int len) throws IOException
      {
          out.writeString(new String(bytes, start, len, StandardCharsets.ISO_8859_1));
      }
  
      @Override
      public void writeFixed(
          byte[] bytes,
          int start,
          int len) throws IOException
      {
          parser.advance(Symbol.FIXED);
          Symbol.IntCheckAction top = (Symbol.IntCheckAction) parser.popSymbol();
          if (len != top.size)
          {
              throw new AvroTypeException(
                  "Incorrect length for fixed binary: expected " + top.size + " but received " + len + " bytes.");
          }
          writeByteArray(bytes, start, len);
      }
  
      @Override
      public void writeEnum(
          int e) throws IOException
      {
          parser.advance(Symbol.ENUM);
          Symbol.EnumLabelsAction top = (Symbol.EnumLabelsAction) parser.popSymbol();
          if (e < 0 || e >= top.size)
          {
              throw new AvroTypeException("Enumeration out of range: max is " + top.size + " but received " + e);
          }
          out.writeString(top.getLabel(e));
      }
  
      @Override
      public void writeArrayStart() throws IOException
      {
          parser.advance(Symbol.ARRAY_START);
          out.writeStartArray();
          push();
          isEmpty.set(depth());
      }
  
      @Override
      public void writeArrayEnd() throws IOException
      {
          if (!isEmpty.get(pos))
          {
              parser.advance(Symbol.ITEM_END);
          }
          pop();
          parser.advance(Symbol.ARRAY_END);
          out.writeEndArray();
      }
  
      @Override
      public void writeMapStart() throws IOException
      {
          push();
          isEmpty.set(depth());
  
          parser.advance(Symbol.MAP_START);
          out.writeStartObject();
      }
  
      @Override
      public void writeMapEnd() throws IOException
      {
          if (!isEmpty.get(pos))
          {
              parser.advance(Symbol.ITEM_END);
          }
          pop();
  
          parser.advance(Symbol.MAP_END);
          out.writeEndObject();
      }
  
      @Override
      public void startItem() throws IOException
      {
          if (!isEmpty.get(pos))
          {
              parser.advance(Symbol.ITEM_END);
          }
          super.startItem();
          isEmpty.clear(depth());
      }
  
      @Override
      public void writeIndex(
          int unionIndex) throws IOException
      {
          parser.advance(Symbol.UNION);
          Symbol.Alternative top = (Symbol.Alternative) parser.popSymbol();
          Symbol symbol = top.getSymbol(unionIndex);
          if (symbol != Symbol.NULL && includeNamespace)
          {
              out.writeStartObject();
              out.writeFieldName(top.getLabel(unionIndex));
              parser.pushSymbol(Symbol.UNION_END);
          }
          parser.pushSymbol(symbol);
      }
  
      @Override
      public Symbol doAction(Symbol input, Symbol top) throws IOException
      {
          if (top instanceof Symbol.FieldAdjustAction)
          {
              Symbol.FieldAdjustAction fa = (Symbol.FieldAdjustAction) top;
              out.writeFieldName(fa.fname);
          }
          else if (top == Symbol.RECORD_START)
          {
              out.writeStartObject();
          }
          else if (top == Symbol.RECORD_END || top == Symbol.UNION_END)
          {
              out.writeEndObject();
          }
          else if (top != Symbol.FIELD_END)
          {
              throw new AvroTypeException("Unknown action symbol " + top);
          }
          return null;
      }
  }
  