YAMLFactory.java

package tools.jackson.dataformat.yaml;

import java.io.*;

import org.snakeyaml.engine.v2.api.DumpSettings;
import org.snakeyaml.engine.v2.api.LoadSettings;
import org.snakeyaml.engine.v2.api.LoadSettingsBuilder;
import org.snakeyaml.engine.v2.common.SpecVersion;
import org.snakeyaml.engine.v2.schema.CoreSchema;
import org.snakeyaml.engine.v2.schema.FailsafeSchema;
import org.snakeyaml.engine.v2.schema.JsonSchema;
import org.snakeyaml.engine.v2.schema.Schema;

import tools.jackson.core.*;
import tools.jackson.core.base.TextualTSFactory;
import tools.jackson.core.io.GuardedOutputStream;
import tools.jackson.core.io.IOContext;

import tools.jackson.dataformat.yaml.util.StringQuotingChecker;

@SuppressWarnings("resource")
public class YAMLFactory
    extends TextualTSFactory
    implements java.io.Serializable
{
    private static final long serialVersionUID = 1L;

    /**
     * Name used to identify YAML format.
     * (and returned by {@link #getFormatName()}
     */
    public final static String FORMAT_NAME_YAML = "YAML";

    /**
     * Bitfield (set of flags) of all generator features that are enabled
     * by default.
     */
    protected final static int DEFAULT_YAML_PARSER_FEATURE_FLAGS = YAMLReadFeature.collectDefaults();

    /**
     * Bitfield (set of flags) of all generator features that are enabled
     * by default.
     */
    protected final static int DEFAULT_YAML_GENERATOR_FEATURE_FLAGS = YAMLWriteFeature.collectDefaults();

    /*
    /**********************************************************************
    /* Configuration
    /**********************************************************************
     */

    /**
     * YAML version for underlying generator to follow, if specified.
     */
    protected final SpecVersion _version;

    /**
     * YAML schema for underlying parser to follow, if specified.
     * @since 3.2
     */
    protected final YAMLSchema _schema;

    /**
     * Helper object used to determine whether property names, String values
     * must be quoted or not.
     */
    protected final StringQuotingChecker _quotingChecker;

    /**
     * Configuration for underlying parser to follow, if specified;
     * left as {@code null} for backwards compatibility (which means
     * whatever default settings {@code snakeyaml-engine} deems best).
     */
    protected final LoadSettings _loadSettings;    

    /**
     * Configuration for underlying generator to follow, if specified;
     * left as {@code null} for backwards compatibility (which means
     * the dumper options are derived based on {@link YAMLWriteFeature}s).
     * <p>
     *     These {@link YAMLWriteFeature}s are ignored if you provide your own DumperOptions:
     *     <ul>
     *         <li>{@code YAMLGenerator.Feature.ALLOW_LONG_KEYS}</li>
     *         <li>{@code YAMLGenerator.Feature.CANONICAL_OUTPUT}</li>
     *         <li>{@code YAMLGenerator.Feature.INDENT_ARRAYS}</li>
     *         <li>{@code YAMLGenerator.Feature.INDENT_ARRAYS_WITH_INDICATOR}</li>
     *         <li>{@code YAMLGenerator.Feature.SPLIT_LINES}</li>
     *         <li>{@code YAMLGenerator.Feature.USE_PLATFORM_LINE_BREAKS}</li>
     *     </ul>
     * </p>
     */
    protected final DumpSettings _dumpSettings;

    /*
    /**********************************************************************
    /* Factory construction, configuration
    /**********************************************************************
     */

    /**
     * Default constructor used to create factory instances that may be
     * used to construct an instance with default settings, instead of
     * using {@link YAMLFactoryBuilder}.
     */
    public YAMLFactory()
    {
        super(StreamReadConstraints.defaults(), StreamWriteConstraints.defaults(),
                ErrorReportConfiguration.defaults(),
                DEFAULT_YAML_PARSER_FEATURE_FLAGS, DEFAULT_YAML_GENERATOR_FEATURE_FLAGS);
        // 26-Jul-2013, tatu: Seems like we should force output as 1.1 but
        //  that adds version declaration which looks ugly...
        _version = null;
        _schema  = null;
        _quotingChecker = StringQuotingChecker.Default.instance();
        _loadSettings = null;
        _dumpSettings = null;
    }

    public YAMLFactory(YAMLFactory src)
    {
        super(src);
        _version = src._version;
        _schema  = src._schema;
        _quotingChecker = src._quotingChecker;
        _loadSettings = src._loadSettings;
        _dumpSettings = src._dumpSettings;
    }

    /**
     * Constructors used by {@link YAMLFactoryBuilder} for instantiation.
     *
     * @since 3.0
     */
    protected YAMLFactory(YAMLFactoryBuilder b)
    {
        super(b);
        _version = b.yamlVersionToWrite();
        _schema  = b.yamlSchema();
        _quotingChecker = b.stringQuotingChecker();

        if (b.loadSettings() == null) {
            LoadSettingsBuilder builder = LoadSettings.builder();
            if (_schema != null) {
                Schema s = switch (_schema) {
                    case FAILSAFE -> new FailsafeSchema();
                    case JSON -> new JsonSchema();
                    case CORE -> new CoreSchema();
                    default -> null;
                };
                if (s != null) {
                    builder.setSchema(s);
                }
            }
            _loadSettings = builder.build();
        }
        else {
            _loadSettings = b.loadSettings();
        }

        _dumpSettings = b.dumpSettings();
    }

    @Override
    public YAMLFactoryBuilder rebuild() {
        return new YAMLFactoryBuilder(this);
    }

    /**
     * Main factory method to use for constructing {@link YAMLFactory} instances with
     * different configuration.
     */
    public static YAMLFactoryBuilder builder() {
        return new YAMLFactoryBuilder();
    }

    @Override
    public YAMLFactory copy() {
        return new YAMLFactory(this);
    }

    /**
     * Instances are immutable so just return `this`
     */
    @Override
    public TokenStreamFactory snapshot() {
        return this;
    }

    /*
    /**********************************************************************
    /* Serializable overrides
    /**********************************************************************
     */

    /**
     * Method that we need to override to actually make restoration go
     * through constructors etc.
     */
    protected Object readResolve() {
        return new YAMLFactory(this);
    }

    /*
    /**********************************************************************
    /* Capability introspection
    /**********************************************************************
     */

    @Override
    public Version version() {
        return PackageVersion.VERSION;
    }

    // No, we can't make use of char[] optimizations
    @Override
    public boolean canUseCharArrays() { return false; }

    @Override
    public boolean canParseAsync() {
        // 31-May-2017, tatu: No async parsing yet
        return false;
    }

    /*
    /**********************************************************************
    /* Format support
    /**********************************************************************
     */

    @Override
    public String getFormatName() {
        return FORMAT_NAME_YAML;
    }

    @Override
    public boolean canUseSchema(FormatSchema schema) {
        return false;
    }

    @Override
    public Class<YAMLReadFeature> getFormatReadFeatureType() {
        return YAMLReadFeature.class;
    }

    @Override
    public Class<YAMLWriteFeature> getFormatWriteFeatureType() {
        return YAMLWriteFeature.class;
    }

    @Override
    public int getFormatReadFeatures() { return _formatReadFeatures; }

    @Override
    public int getFormatWriteFeatures() { return _formatWriteFeatures; }

    public boolean isEnabled(YAMLReadFeature f) {
        return (_formatReadFeatures & f.getMask()) != 0;
    }

    public boolean isEnabled(YAMLWriteFeature f) {
        return (_formatWriteFeatures & f.getMask()) != 0;
    }

    /*
    /**********************************************************************
    /* Factory methods: parsers
    /**********************************************************************
     */

    @Override
    protected YAMLParser _createParser(ObjectReadContext readCtxt, IOContext ioCtxt,
            InputStream in) {
        final int stdFeatures = readCtxt.getStreamReadFeatures(_streamReadFeatures);
        final Reader r = _createReader(in, null, ioCtxt, stdFeatures);
        // 18-Sep-2026: [dataformats-text#718] `UTF8Reader` holds a recycled buffer and
        //   knows whether it may close the caller's `InputStream`, so it must always be
        //   closed. Other `Reader`s (incl. ones a subclass may return) get the legacy
        //   treatment: closed only if auto-closing is enabled.
        return new YAMLParser(readCtxt, ioCtxt,
                _getBufferRecycler(),
                stdFeatures,
                readCtxt.getFormatReadFeatures(_formatReadFeatures),
                _loadSettings,
                r, (r instanceof UTF8Reader));
    }

    @Override
    protected YAMLParser _createParser(ObjectReadContext readCtxt, IOContext ioCtxt,
            Reader r) {
        return new YAMLParser(readCtxt, ioCtxt,
                _getBufferRecycler(), 
                readCtxt.getStreamReadFeatures(_streamReadFeatures),
                readCtxt.getFormatReadFeatures(_formatReadFeatures),
                _loadSettings,
                r, false);
    }

    @Override
    protected YAMLParser _createParser(ObjectReadContext readCtxt, IOContext ioCtxt,
            char[] data, int offset, int len,
            boolean recyclable) {
        return new YAMLParser(readCtxt, ioCtxt, _getBufferRecycler(),
                readCtxt.getStreamReadFeatures(_streamReadFeatures),
                readCtxt.getFormatReadFeatures(_formatReadFeatures),
                _loadSettings,
                new CharArrayReader(data, offset, len), true);
    }

    @Override
    protected YAMLParser _createParser(ObjectReadContext readCtxt, IOContext ioCtxt,
            byte[] data, int offset, int len) {
        return new YAMLParser(readCtxt, ioCtxt, _getBufferRecycler(),
                readCtxt.getStreamReadFeatures(_streamReadFeatures),
                readCtxt.getFormatReadFeatures(_formatReadFeatures),
                _loadSettings,
                _createReader(data, offset, len, null, ioCtxt), true);
    }

    @Override
    protected JsonParser _createParser(ObjectReadContext readCtxt, IOContext ioCtxt,
            DataInput input) {
        return _unsupported();
    }

    /*
    /**********************************************************************
    /* Factory methods: generators
    /**********************************************************************
     */

    @Override
    protected YAMLGenerator _createGenerator(ObjectWriteContext writeCtxt,
            IOContext ioCtxt, Writer out)
    {
        return new YAMLGenerator(writeCtxt, ioCtxt,
                writeCtxt.getStreamWriteFeatures(_streamWriteFeatures),
                writeCtxt.getFormatWriteFeatures(_formatWriteFeatures),
                _quotingChecker,
                out, _version, _dumpSettings);
    }

    @Override
    protected YAMLGenerator _createUTF8Generator(ObjectWriteContext writeCtxt,
            IOContext ioCtxt, OutputStream out)
    {
        // [dataformats-text#735] (as with toml): Writer is constructed (and hence owned) by us, so the
        // generator must always flush and close it to get its buffered content into the
        // stream. It is shielded from the caller's stream, which the generator itself
        // flushes or closes as per `FLUSH_PASSED_TO_STREAM` and `AUTO_CLOSE_TARGET`
        // (as enabled at that point).
        return new YAMLGenerator(writeCtxt, ioCtxt,
                writeCtxt.getStreamWriteFeatures(_streamWriteFeatures),
                writeCtxt.getFormatWriteFeatures(_formatWriteFeatures),
                _quotingChecker,
                _createWriter(ioCtxt, new GuardedOutputStream(out), JsonEncoding.UTF8), out,
                _version, _dumpSettings);
    }

    @Override
    protected Writer _createWriter(IOContext ioCtxt, OutputStream out, JsonEncoding enc) {
        if (enc == JsonEncoding.UTF8) {
            return new UTF8Writer(out);
        }
        try {
            return new OutputStreamWriter(out, enc.getJavaName());
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
    }

    /*
    /**********************************************************************
    /* Internal methods
    /**********************************************************************
     */

    /**
     * @param streamReadFeatures Effective {@link StreamReadFeature}s of the parser being
     *    constructed: note that these may differ from this factory's defaults, since
     *    {@code ObjectReader} may override them on a per-call basis.
     *
     * @since 3.3
     */
    protected Reader _createReader(InputStream in, JsonEncoding enc, IOContext ctxt,
            int streamReadFeatures)
    {
        if (enc == null) {
            enc = JsonEncoding.UTF8;
        }
        // default to UTF-8 if encoding missing
        if (enc == JsonEncoding.UTF8) {
            // 08-Sep-2026, pjfanning: [dataformats-text#718] must use the parser's
            //   effective features here; `YAMLParser._closeInput()` decides whether to
            //   close this Reader based on those, and if the two disagree the caller's
            //   `InputStream` is silently left open
            boolean autoClose = ctxt.isResourceManaged()
                    || StreamReadFeature.AUTO_CLOSE_SOURCE.enabledIn(streamReadFeatures);
            return new UTF8Reader(in, autoClose);
//          return new InputStreamReader(in, UTF8);
        }
        try {
            return new InputStreamReader(in, enc.getJavaName());
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
    }

    protected Reader _createReader(byte[] data, int offset, int len,
            JsonEncoding enc, IOContext ctxt)
    {
        if (enc == null) {
            enc = JsonEncoding.UTF8;
        }
        // default to UTF-8 if encoding missing
        if (enc == null || enc == JsonEncoding.UTF8) {
            return new UTF8Reader(data, offset, len, true);
        }
        ByteArrayInputStream in = new ByteArrayInputStream(data, offset, len);
        try {
            return new InputStreamReader(in, enc.getJavaName());
        } catch (IOException e) {
            throw _wrapIOFailure(e);
        }
    }
}