content: // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
  // SPDX-License-Identifier: Apache-2.0
  package com.amazon.ion.system;
  
  import com.amazon.ion.util.GzipStreamInterceptor;
  import com.amazon.ion.IonBufferConfiguration;
  import com.amazon.ion.IonCatalog;
  import com.amazon.ion.IonException;
  import com.amazon.ion.IonReader;
  import com.amazon.ion.IonStruct;
  import com.amazon.ion.IonSystem;
  import com.amazon.ion.IonTextReader;
  import com.amazon.ion.IonValue;
  import com.amazon.ion.util.InputStreamInterceptor;
  import com.amazon.ion.impl._Private_IonReaderBuilder;
  
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.Reader;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.Iterator;
  import java.util.List;
  import java.util.ServiceLoader;
  
  /**
   * Build a new {@link IonReader} from the given {@link IonCatalog} and data
   * source. A data source is required, while an IonCatalog is optional. If no
   * IonCatalog is provided, an empty {@link SimpleCatalog} will be used.
   * <p>
   * {@link IonReader}s parse incrementally, so syntax errors in the input data
   * will not be detected as side effects of any of the {@code build} methods
   * in this class.
   * 
   * <h2>Obtaining and Usage</h2>
   * An {@code IonReaderBuilder} with the default configuration may be constructed
   * as follows. This builder will construct {@code IonReader} instances which
   * can read both text and binary Ion data and is appropriate for simple use
   * cases or when no IonCatalog is in use.
   * {@snippet :
   * IonReaderBuilder readerBuilder = IonReaderBuilder.standard();
   * }
   * {@code IonReaderBuilder}s can be configured by chaining calls to {@code with*()}
   * configuration methods. Below is an example of a builder configured to
   * incrementally read Ion binary data, use a custom initial buffer size, throw
   * on inputs that would require a buffer over a specified size, and use a
   * user-provided IonCatalog.
   * {@snippet :
   * // Create an IonCatalog and IonBufferConfiguration to use with IonReaderBuilder
   * final IonCatalog catalog = new SimpleCatalog();
   * final IonBufferConfiguration bufferConfiguration = IonBufferConfiguration.Builder.standard()
   *       .onOversizedSymbolTable(() -> { throw new IllegalStateException("Oversized system table encountered"); })
   *       .onOversizedValue(() -> { throw new IllegalStateException("Oversized value encountered"); })
   *       .withInitialBufferSize(1024)
   *       .withMaximumBufferSize(1024 * 1024)
   *       .build();
   *
   * IonReaderBuilder readerBuilder = IonReaderBuilder.standard()
   *     .withIncrementalReadingEnabled(true)
   *     .withBufferConfiguration(bufferConfiguration)
   *     .withCatalog(catalog);
   * }
   * 
   * <h3>Building a Reader over a Data Source</h3>
   * An {@code IonReader} may be obtained from a builder by calling {@code build}
   * over the appropriate data source. Below is an example of a reader being constructed
   * over a string containing Ion text from a builder with the default configuration.
   * {@snippet :
   * IonReaderBuilder readerBuilder = IonReaderBuilder.standard();
   * final String helloWorld = "{hello: \"world\"}";
   * try (final IonReader reader = readerBuilder.build(helloWorld)) {
   *     reader.next();
   *     reader.stepIn();
   *     reader.next();
   *     System.out.println(reader.getFieldName() + " " + reader.stringValue());  // prints "hello world"
   * }
   * }
   * Builders can build an IonReader over a string, {@code byte[]} array,
   * {@link java.io.InputStream}, {@link java.io.Reader}, or existing {@link com.amazon.ion.IonValue}
   * data model. Building a reader over a byte array allows specifying the start
   * index and length of the data to be read. Readers built over {@code byte[]}
   * arrays or {@code InputStream}s are capable of reading both binary and text
   * Ion; readers built over strings and {@code Reader} instances are only capable of
   * reading text Ion.
   */
  @SuppressWarnings("deprecation")
  public abstract class IonReaderBuilder
  {
  
      // Default stream interceptors, which always begin with the GZIP interceptor.
      private static final List<InputStreamInterceptor> DEFAULT_STREAM_INTERCEPTORS = Collections.singletonList(GzipStreamInterceptor.INSTANCE);
  
      // Stream interceptors detected by the ClassLoader.
      // Note: each ClassLoader may have access to a different list of detected interceptors, but static initialization
      // is performed once per load of the class, and is guaranteed thread-safe. Therefore, there is no need to manually
      // maintain a thread-safe cache of detected interceptors.
      private static final List<InputStreamInterceptor> DETECTED_STREAM_INTERCEPTORS = Collections.unmodifiableList(detectStreamInterceptorsOnClasspath());
  
  
      private IonCatalog catalog = null;
      private boolean isIncrementalReadingEnabled = false;
      private IonBufferConfiguration bufferConfiguration = IonBufferConfiguration.DEFAULT;
      private List<InputStreamInterceptor> streamInterceptors = null;
  
      protected IonReaderBuilder()
      {
      }
  
      protected IonReaderBuilder(IonReaderBuilder that)
      {
          this.catalog = that.catalog;
          this.isIncrementalReadingEnabled = that.isIncrementalReadingEnabled;
          this.bufferConfiguration = that.bufferConfiguration;
          this.streamInterceptors = that.streamInterceptors == null ? null : new ArrayList<>(that.streamInterceptors);
      }
  
      /**
       * The standard builder of {@link IonReader}s, with all configuration
       * properties having their default values.
       *
       * @return a new, mutable builder instance.
       */
      public static IonReaderBuilder standard()
      {
          return new _Private_IonReaderBuilder.Mutable();
      }
  
      /**
       * Creates a mutable copy of this builder.
       *
       * @return a new builder with the same configuration as {@code this}.
       */
      public IonReaderBuilder copy()
      {
          return new _Private_IonReaderBuilder.Mutable(this);
      }
  
      /**
       * Returns an immutable builder configured exactly like this one.
       *
       * @return this builder instance, if immutable;
       * otherwise an immutable copy of this builder.
       */
      public IonReaderBuilder immutable()
      {
          return this;
      }
  
      /**
       * Returns a mutable builder configured exactly like this one.
       *
       * @return this instance, if mutable;
       * otherwise a mutable copy of this instance.
       */
      public IonReaderBuilder mutable()
      {
          return copy();
      }
  
      /** NOT FOR APPLICATION USE! */
      protected void mutationCheck()
      {
          throw new UnsupportedOperationException("This builder is immutable");
      }
  
      /**
       * Declares the catalog to use when building an {@link IonReader},
       * returning a new mutable builder the current one is immutable.
       *
       * @param catalog the catalog to use in built readers.
       *  If null, a new {@link SimpleCatalog} will be used.
       *
       * @return this builder instance, if mutable;
       * otherwise a mutable copy of this builder.
       *
       * @see #setCatalog(IonCatalog)
       * @see #withCatalog(IonCatalog)
       */
      public IonReaderBuilder withCatalog(IonCatalog catalog)
      {
          IonReaderBuilder b = mutable();
          b.setCatalog(catalog);
          return b;
      }
  
      /**
       * Sets the catalog to use when building an {@link IonReader}.
       *
       * @param catalog the catalog to use in built readers.
       *  If null, a new {@link SimpleCatalog} will be used.
       *
       * @see #getCatalog()
       * @see #withCatalog(IonCatalog)
       *
       * @throws UnsupportedOperationException if this builder is immutable.
       */
      public void setCatalog(IonCatalog catalog)
      {
          mutationCheck();
          this.catalog = catalog;
      }
  
      /**
       * Gets the catalog to use when building an {@link IonReader}, or null
       * if none has been manually set. The catalog is needed to resolve shared
       * symbol table imports.
       *
       * @see #setCatalog(IonCatalog)
       * @see #withCatalog(IonCatalog)
       */
      public IonCatalog getCatalog()
      {
          return catalog;
      }
  
      protected IonCatalog validateCatalog()
      {
          // matches behavior in IonSystemBuilder when no catalog provided
          return catalog != null ? catalog : new SimpleCatalog();
      }
  
      /**
       * <p>
       * Determines whether the IonReader will allow incremental reading of binary Ion data. When enabled, if
       * {@link IonReader#next()} returns {@code null} at the top-level, it indicates that there is not enough data
       * in the stream to complete a top-level value. The user may wait for more data to become available in the stream
       * and call {@link IonReader#next()} again to continue reading. Unlike the non-incremental reader, the incremental
       * reader will never throw an exception due to unexpected EOF during {@code next()}. If, however,
       * {@link IonReader#close()} is called when an incomplete value is buffered, an {@link IonException} will be raised.
       * </p>
       * <p>
       * There is currently no incremental text IonReader, so for text data a non-incremental IonReader will be
       * returned regardless of the value of this option. If incremental text reading is supported in the future, it
       * may be enabled via this option.
       * </p>
       * <p>
       * There is one caveat to note when using this option: the incremental implementation must be able to buffer an
       * entire top-level value in memory. This will not be a problem for the vast majority of Ion streams, as it is rare
       * for a single top-level value or symbol table to exceed a few megabytes in size. However, if the size of the
       * stream's values risks exceeding the available memory, then this option must not be enabled.
       * </p>
       * @param isEnabled true if the option is enabled; otherwise, false.
       *
       * @return this builder instance, if mutable;
       * otherwise a mutable copy of this builder.
       *
       * @see #setIncrementalReadingEnabled()
       * @see #setIncrementalReadingDisabled()
       */
      public IonReaderBuilder withIncrementalReadingEnabled(boolean isEnabled) {
          IonReaderBuilder b = mutable();
          if (isEnabled) {
              b.setIncrementalReadingEnabled();
          } else {
              b.setIncrementalReadingDisabled();
          }
          return b;
      }
  
      /**
       * @see #withIncrementalReadingEnabled(boolean)
       */
      public void setIncrementalReadingEnabled() {
          mutationCheck();
          isIncrementalReadingEnabled = true;
      }
  
      /**
       * @see #withIncrementalReadingEnabled(boolean)
       */
      public void setIncrementalReadingDisabled() {
          mutationCheck();
          isIncrementalReadingEnabled = false;
      }
  
      /**
       * @see #withIncrementalReadingEnabled(boolean)
       * @return true if incremental reading is enabled; otherwise, false.
       */
      public boolean isIncrementalReadingEnabled() {
          return isIncrementalReadingEnabled;
      }
  
      /**
       * Sets the buffer configuration. This can be used, for example, to set a maximum buffer size
       * and receive notifications when values would exceed this size. Currently, this is ignored unless incremental
       * reading has been enabled via {@link #withIncrementalReadingEnabled(boolean)}) or
       * {@link #setIncrementalReadingEnabled()}. This configuration is optional. If not provided, the buffer size will
       * be limited only by the available memory.
       *
       * @param configuration the configuration.
       *
       * @return this builder instance, if mutable;
       * otherwise a mutable copy of this builder.
       * 
       * @see #setBufferConfiguration(IonBufferConfiguration)
       */
      public IonReaderBuilder withBufferConfiguration(IonBufferConfiguration configuration) {
          IonReaderBuilder b = mutable();
          b.setBufferConfiguration(configuration);
          return b;
      }
  
      /**
       * @see #withBufferConfiguration(IonBufferConfiguration)
       */
      public void setBufferConfiguration(IonBufferConfiguration configuration) {
          mutationCheck();
          if (configuration == null) {
              throw new IllegalArgumentException("Configuration must not be null. To use the default configuration, provide IonBufferConfiguration.DEFAULT.");
          }
          bufferConfiguration = configuration;
      }
  
      /**
       * @see #withBufferConfiguration(IonBufferConfiguration)
       * @return the current configuration.
       */
      public IonBufferConfiguration getBufferConfiguration() {
          return bufferConfiguration;
      }
  
      /**
       * Adds an {@link InputStreamInterceptor} to the end of the list that the builder will attempt
       * to apply to a stream before creating {@link IonReader} instances over that stream.
       * {@link GzipStreamInterceptor} is always consulted first, and need not be added. The first
       * interceptor in the list that matches the stream will be used; if any chaining of interceptors
       * is required, it is up to the caller to provide a custom interceptor implementation to
       * achieve this.
       * <p>
       * Users may also or instead register implementations as service providers on the classpath.
       * See {@link ServiceLoader} for details about how to do this.
       * <p>
       * The list of stream interceptors available to the reader always begins with
       * {@link GzipStreamInterceptor} and is followed by:
       * <ol>
       *     <li>any stream interceptors detected on the classpath using
       *     {@link ServiceLoader#load(Class)}, then</li>
       *     <li>any stream interceptor(s) added by calling this method.</li>
       * </ol>
       *
       * @param streamInterceptor the stream interceptor to add.
       *
       * @return this builder instance, if mutable;
       * otherwise a mutable copy of this builder.
       */
      public IonReaderBuilder addInputStreamInterceptor(InputStreamInterceptor streamInterceptor) {
          IonReaderBuilder b = mutable();
          if (b.streamInterceptors == null) {
              // 4 is arbitrary, but more would be very rare.
              b.streamInterceptors = new ArrayList<>(DETECTED_STREAM_INTERCEPTORS.size() + 4);
              b.streamInterceptors.addAll(DETECTED_STREAM_INTERCEPTORS);
          }
          b.streamInterceptors.add(streamInterceptor);
          return b;
      }
  
      /**
       * Detects implementations of {@link InputStreamInterceptor} available to the {@link ClassLoader} that loaded
       * this class, appending any implementations found to the list of stream interceptors enabled by default.
       * @return the stream interceptors.
       */
      private static List<InputStreamInterceptor> detectStreamInterceptorsOnClasspath() {
          ServiceLoader<InputStreamInterceptor> loader = ServiceLoader.load(
              InputStreamInterceptor.class,
              // The ClassLoader used to load this class. Each ClassLoader may have access to different resources.
              IonReaderBuilder.class.getClassLoader()
          );
          Iterator<InputStreamInterceptor> interceptorIterator = loader.iterator();
          if (!interceptorIterator.hasNext()) {
              // Avoid allocating a new list in the common case: no custom interceptors detected.
              return DEFAULT_STREAM_INTERCEPTORS;
          }
          List<InputStreamInterceptor> interceptorsOnClasspath = new ArrayList<>(4); // 4 is arbitrary, but more would be very rare.
          interceptorsOnClasspath.addAll(DEFAULT_STREAM_INTERCEPTORS);
          interceptorIterator.forEachRemaining(interceptorsOnClasspath::add);
          return interceptorsOnClasspath;
      }
  
      /**
       * Gets the {@link InputStreamInterceptor} instances available to this builder. The returned list will always begin
       * with the default stream interceptor, which detects GZIP. Any stream interceptor(s) detected on the classpath
       * by {@link ServiceLoader#load(Class)} will immediately follow. Any stream interceptor(s) manually added using
       * {@link #addInputStreamInterceptor(InputStreamInterceptor)} will occur at the end of the list.
       * @return an unmodifiable view of the stream interceptors currently configured.
       */
      public List<InputStreamInterceptor> getInputStreamInterceptors() {
          if (streamInterceptors == null) {
              return DETECTED_STREAM_INTERCEPTORS;
          }
          return Collections.unmodifiableList(streamInterceptors);
      }
  
      /**
       * Based on the builder's configuration properties, creates a new IonReader
       * instance over the given block of Ion data, detecting whether it's text or
       * binary data.
       * <p>
       * This method will auto-detect and uncompress GZIPped Ion data.
       *
       * @param ionData the source of the Ion data, which may be either Ion binary
       * data or UTF-8 Ion text. The reader retains a reference to the array, so
       * its data must not be modified while the reader is active. Must not be
       * null.
       *
       * @return a new {@link IonReader} instance; not {@code null}.
       *
       * @see IonSystem#newReader(byte[])
       */
      public IonReader build(byte[] ionData)
      {
          return build(ionData, 0, ionData.length);
      }
  
      /**
       * Based on the builder's configuration properties, creates a new IonReader
       * instance over the given block of Ion data, detecting whether it's text or
       * binary data.
       * <p>
       * This method will auto-detect and uncompress GZIPped Ion data.
       *
       * @param ionData the source of the Ion data, which is used only within the
       * range of bytes starting at {@code offset} for {@code len} bytes.
       * The data in that range may be either Ion binary data or UTF-8 Ion text.
       * The reader retains a reference to the array, so its data must not be
       * modified while the reader is active. Must not be null.
       * @param offset must be non-negative and less than {@code ionData.length}.
       * @param length must be non-negative and {@code offset+length} must not
       * exceed {@code ionData.length}.
       *
       * @see IonSystem#newReader(byte[], int, int)
       */
      public abstract IonReader build(byte[] ionData, int offset, int length);
  
      /**
       * Based on the builder's configuration properties, creates a new IonReader
       * instance over the given stream of Ion data, detecting whether it's text or
       * binary data.
       * <p>
       * This method will auto-detect and uncompress GZIPped Ion data.
       * <p>
       * Because this library performs its own buffering, it's recommended that
       * users avoid adding additional buffering to the given stream.
       *
       * @param ionData the source of the Ion data, which may be either Ion binary
       * data or UTF-8 Ion text. Must not be null.
       *
       * @return a new reader instance.
       * Callers must call {@link IonReader#close()} when finished with it.
       *
       * @throws IonException if the source throws {@link IOException}.
       *
       * @see IonSystem#newReader(InputStream)
       */
      public abstract IonReader build(InputStream ionData);
  
      /**
       * Based on the builder's configuration properties, creates a new
       * {@link IonReader} instance over Ion text data.
       * <p>
       * Applications should generally use {@link #build(InputStream)}
       * whenever possible, since this library has much faster Unicode decoding
       * than the Java IO framework.
       * <p>
       * Because this library performs its own buffering, it's recommended that
       * you avoid adding additional buffering to the given stream.
       *
       * @param ionText the source of the Ion text data. Must not be null.
       *
       * @throws IonException if the source throws {@link IOException}.
       *
       * @see IonSystem#newReader(Reader)
       */
      public abstract IonReader build(Reader ionText);
  
      /**
       * Based on the builder's configuration properties, creates a new
       * {@link IonReader} instance over an {@link IonValue} data model. Typically
       * this is used to iterate over a collection, such as an {@link IonStruct}.
       *
       * The given value and its children, if any, must not be modified until after
       * the IonReader constructed by this method is closed. Violating this
       * constraint results in undefined behavior.
       *
       * @param value must not be null.
       *
       * @see IonSystem#newReader(IonValue)
       */
      public abstract IonReader build(IonValue value);
  
      /**
       * Based on the builder's configuration properties, creates an new
       * {@link IonReader} instance over Ion text data.
       *
       * @param ionText the source of the Ion text data. Must not be null.
       *
       * @see IonSystem#newReader(String)
       */
      public abstract IonTextReader build(String ionText);
  
  }
  