package com.fasterxml.jackson.dataformat.yaml;

import java.io.*;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.regex.Pattern;

import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.DumperOptions.FlowStyle;
import org.yaml.snakeyaml.emitter.Emitter;
import org.yaml.snakeyaml.events.*;
import org.yaml.snakeyaml.nodes.Tag;

import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.core.base.GeneratorBase;
import com.fasterxml.jackson.core.json.JsonWriteContext;
import com.fasterxml.jackson.core.io.IOContext;

public class YAMLGenerator extends GeneratorBase
{
    
Enumeration that defines all togglable features for YAML generators
/** * Enumeration that defines all togglable features for YAML generators */
public enum Feature implements FormatFeature // since 2.9 {
Whether we are to write an explicit document start marker ("---") or not.
Since:2.3
/** * Whether we are to write an explicit document start marker ("---") * or not. * * @since 2.3 */
WRITE_DOC_START_MARKER(true),
Whether to use YAML native Object Id construct for indicating type (true); or "generic" Object Id mechanism (false). Former works better for systems that are YAML-centric; latter may be better choice for interoperability, when converting between formats or accepting other formats.
Since:2.5
/** * Whether to use YAML native Object Id construct for indicating type (true); * or "generic" Object Id mechanism (false). Former works better for systems that * are YAML-centric; latter may be better choice for interoperability, when * converting between formats or accepting other formats. * * @since 2.5 */
USE_NATIVE_OBJECT_ID(true),
Whether to use YAML native Type Id construct for indicating type (true); or "generic" type property (false). Former works better for systems that are YAML-centric; latter may be better choice for interoperability, when converting between formats or accepting other formats.
Since:2.5
/** * Whether to use YAML native Type Id construct for indicating type (true); * or "generic" type property (false). Former works better for systems that * are YAML-centric; latter may be better choice for interoperability, when * converting between formats or accepting other formats. * * @since 2.5 */
USE_NATIVE_TYPE_ID(true),
Do we try to force so-called canonical output or not.
/** * Do we try to force so-called canonical output or not. */
CANONICAL_OUTPUT(false),
Options passed to SnakeYAML that determines whether longer textual content gets automatically split into multiple lines or not.

Feature is enabled by default to conform to SnakeYAML defaults as well as backwards compatibility with 2.5 and earlier versions.

Since:2.6
/** * Options passed to SnakeYAML that determines whether longer textual content * gets automatically split into multiple lines or not. *<p> * Feature is enabled by default to conform to SnakeYAML defaults as well as * backwards compatibility with 2.5 and earlier versions. * * @since 2.6 */
SPLIT_LINES(true),
Whether strings will be rendered without quotes (true) or with quotes (false, default).

Minimized quote usage makes for more human readable output; however, content is limited to printable characters according to the rules of literal block style.

Since:2.7
/** * Whether strings will be rendered without quotes (true) or * with quotes (false, default). *<p> * Minimized quote usage makes for more human readable output; however, content is * limited to printable characters according to the rules of * <a href="http://www.yaml.org/spec/1.2/spec.html#style/block/literal">literal block style</a>. * * @since 2.7 */
MINIMIZE_QUOTES(false),
Whether numbers stored as strings will be rendered with quotes (true) or without quotes (false, default) when MINIMIZE_QUOTES is enabled.

Minimized quote usage makes for more human readable output; however, content is limited to printable characters according to the rules of literal block style.

Since:2.8.2
/** * Whether numbers stored as strings will be rendered with quotes (true) or * without quotes (false, default) when MINIMIZE_QUOTES is enabled. *<p> * Minimized quote usage makes for more human readable output; however, content is * limited to printable characters according to the rules of * <a href="http://www.yaml.org/spec/1.2/spec.html#style/block/literal">literal block style</a>. * * @since 2.8.2 */
ALWAYS_QUOTE_NUMBERS_AS_STRINGS(false),
Whether for string containing newlines a literal block style should be used. This automatically enabled when MINIMIZE_QUOTES is set.

The content of such strings is limited to printable characters according to the rules of literal block style.

Since:2.9
/** * Whether for string containing newlines a <a href="http://www.yaml.org/spec/1.2/spec.html#style/block/literal">literal block style</a> * should be used. This automatically enabled when {@link #MINIMIZE_QUOTES} is set. * <p> * The content of such strings is limited to printable characters according to the rules of * <a href="http://www.yaml.org/spec/1.2/spec.html#style/block/literal">literal block style</a>. * * @since 2.9 */
LITERAL_BLOCK_STYLE(false),
Feature enabling of which adds indentation for array entry generation (default indentation being 2 spaces).

Default value is `false` for backwards compatibility

Since:2.9
/** * Feature enabling of which adds indentation for array entry generation * (default indentation being 2 spaces). *<p> * Default value is `false` for backwards compatibility * * @since 2.9 */
INDENT_ARRAYS(false) ; protected final boolean _defaultState; protected final int _mask;
Method that calculates bit set (flags) of all features that are enabled by default.
/** * Method that calculates bit set (flags) of all features that * are enabled by default. */
public static int collectDefaults() { int flags = 0; for (Feature f : values()) { if (f.enabledByDefault()) { flags |= f.getMask(); } } return flags; } private Feature(boolean defaultState) { _defaultState = defaultState; _mask = (1 << ordinal()); } @Override public boolean enabledByDefault() { return _defaultState; } @Override public boolean enabledIn(int flags) { return (flags & _mask) != 0; } @Override public int getMask() { return _mask; } } /* /********************************************************** /* Internal constants /********************************************************** */ protected final static long MIN_INT_AS_LONG = (long) Integer.MIN_VALUE; protected final static long MAX_INT_AS_LONG = (long) Integer.MAX_VALUE; protected final static Pattern PLAIN_NUMBER_P = Pattern.compile("[0-9]*(\\.[0-9]*)?"); protected final static String TAG_BINARY = Tag.BINARY.toString(); /* /********************************************************** /* Configuration /********************************************************** */ final protected IOContext _ioContext;
Bit flag composed of bits that indicate which Features are enabled.
/** * Bit flag composed of bits that indicate which * {@link YAMLGenerator.Feature}s * are enabled. */
protected int _formatFeatures; protected Writer _writer; protected DumperOptions _outputOptions; // for field names, leave out quotes private final static Character STYLE_NAME = null; // numbers, booleans, should use implicit private final static Character STYLE_SCALAR = null; // Strings quoted for fun private final static Character STYLE_QUOTED = Character.valueOf('"'); // Strings in literal (block) style private final static Character STYLE_LITERAL = Character.valueOf('|'); // Which flow style to use for Base64? Maybe basic quoted? // 29-Nov-2017, tatu: Actually SnakeYAML uses block style so: private final static Character STYLE_BASE64 = STYLE_LITERAL; private final static Character STYLE_PLAIN = null; /* /********************************************************** /* Output state /********************************************************** */ protected Emitter _emitter;
YAML supports native Object identifiers, so databinder may indicate need to output one.
/** * YAML supports native Object identifiers, so databinder may indicate * need to output one. */
protected String _objectId;
YAML supports native Type identifiers, so databinder may indicate need to output one.
/** * YAML supports native Type identifiers, so databinder may indicate * need to output one. */
protected String _typeId; /* /********************************************************** /* Life-cycle /********************************************************** */ public YAMLGenerator(IOContext ctxt, int jsonFeatures, int yamlFeatures, ObjectCodec codec, Writer out, org.yaml.snakeyaml.DumperOptions.Version version) throws IOException { super(jsonFeatures, codec); _ioContext = ctxt; _formatFeatures = yamlFeatures; _writer = out; _outputOptions = buildDumperOptions(jsonFeatures, yamlFeatures, version); _emitter = new Emitter(_writer, _outputOptions); // should we start output now, or try to defer? _emitter.emit(new StreamStartEvent(null, null)); Map<String,String> noTags = Collections.emptyMap(); boolean startMarker = Feature.WRITE_DOC_START_MARKER.enabledIn(yamlFeatures); _emitter.emit(new DocumentStartEvent(null, null, startMarker, version, // for 1.10 was: ((version == null) ? null : version.getArray()), noTags)); } protected DumperOptions buildDumperOptions(int jsonFeatures, int yamlFeatures, org.yaml.snakeyaml.DumperOptions.Version version) { DumperOptions opt = new DumperOptions(); // would we want canonical? if (Feature.CANONICAL_OUTPUT.enabledIn(_formatFeatures)) { opt.setCanonical(true); } else { opt.setCanonical(false); // if not, MUST specify flow styles opt.setDefaultFlowStyle(FlowStyle.BLOCK); } // split-lines for text blocks? opt.setSplitLines(Feature.SPLIT_LINES.enabledIn(_formatFeatures)); // array indentation? if (Feature.INDENT_ARRAYS.enabledIn(_formatFeatures)) { // But, wrt [dataformats-text#34]: need to set both to diff values to work around bug // (otherwise indentation level is "invisible". Note that this should NOT be necessary // but is needed up to at least SnakeYAML 1.18. // Also looks like all kinds of values do work, except for both being 2... weird. opt.setIndicatorIndent(1); opt.setIndent(2); } return opt; } /* /********************************************************** /* Versioned /********************************************************** */ @Override public Version version() { return PackageVersion.VERSION; } /* /********************************************************** /* Overridden methods, configuration /********************************************************** */
Not sure what to do here; could reset indentation to some value maybe?
/** * Not sure what to do here; could reset indentation to some value maybe? */
@Override public YAMLGenerator useDefaultPrettyPrinter() { return this; }
Not sure what to do here; will always indent, but uses YAML-specific settings etc.
/** * Not sure what to do here; will always indent, but uses * YAML-specific settings etc. */
@Override public YAMLGenerator setPrettyPrinter(PrettyPrinter pp) { return this; } @Override public Object getOutputTarget() { return _writer; }
SnakeYAML does not expose buffered content amount, so we can only return -1 from here
/** * SnakeYAML does not expose buffered content amount, so we can only return * <code>-1</code> from here */
@Override public int getOutputBuffered() { return -1; } @Override public int getFormatFeatures() { return _formatFeatures; } @Override public JsonGenerator overrideFormatFeatures(int values, int mask) { // 14-Mar-2016, tatu: Should re-configure, but unfortunately most // settings passed via options passed to constructor of Emitter _formatFeatures = (_formatFeatures & ~mask) | (values & mask); return this; } @Override public boolean canUseSchema(FormatSchema schema) { return false; } @Override public boolean canWriteFormattedNumbers() { return true; } //@Override public void setSchema(FormatSchema schema) /* /********************************************************** /* Extended API, configuration /********************************************************** */ public YAMLGenerator enable(Feature f) { _formatFeatures |= f.getMask(); return this; } public YAMLGenerator disable(Feature f) { _formatFeatures &= ~f.getMask(); return this; } public final boolean isEnabled(Feature f) { return (_formatFeatures & f.getMask()) != 0; } public YAMLGenerator configure(Feature f, boolean state) { if (state) { enable(f); } else { disable(f); } return this; } /* /********************************************************************** /* Overridden methods; writing field names /********************************************************************** */ /* And then methods overridden to make final, streamline some * aspects... */ @Override public final void writeFieldName(String name) throws IOException { if (_writeContext.writeFieldName(name) == JsonWriteContext.STATUS_EXPECT_VALUE) { _reportError("Can not write a field name, expecting a value"); } _writeFieldName(name); } @Override public final void writeFieldName(SerializableString name) throws IOException { // Object is a value, need to verify it's allowed if (_writeContext.writeFieldName(name.getValue()) == JsonWriteContext.STATUS_EXPECT_VALUE) { _reportError("Can not write a field name, expecting a value"); } _writeFieldName(name.getValue()); } @Override public final void writeStringField(String fieldName, String value) throws IOException { if (_writeContext.writeFieldName(fieldName) == JsonWriteContext.STATUS_EXPECT_VALUE) { _reportError("Can not write a field name, expecting a value"); } _writeFieldName(fieldName); writeString(value); } private final void _writeFieldName(String name) throws IOException { _writeScalar(name, "string", STYLE_NAME); } /* /********************************************************** /* Public API: low-level I/O /********************************************************** */ @Override public final void flush() throws IOException { _writer.flush(); } @Override public void close() throws IOException { if (!isClosed()) { _emitter.emit(new DocumentEndEvent(null, null, false)); _emitter.emit(new StreamEndEvent(null, null)); super.close(); _writer.close(); } } /* /********************************************************** /* Public API: structural output /********************************************************** */ @Override public final void writeStartArray() throws IOException { _verifyValueWrite("start an array"); _writeContext = _writeContext.createChildArrayContext(); Boolean style = _outputOptions.getDefaultFlowStyle().getStyleBoolean(); String yamlTag = _typeId; boolean implicit = (yamlTag == null); String anchor = _objectId; if (anchor != null) { _objectId = null; } _emitter.emit(new SequenceStartEvent(anchor, yamlTag, implicit, null, null, style)); } @Override public final void writeEndArray() throws IOException { if (!_writeContext.inArray()) { _reportError("Current context not Array but "+_writeContext.typeDesc()); } // just to make sure we don't "leak" type ids _typeId = null; _writeContext = _writeContext.getParent(); _emitter.emit(new SequenceEndEvent(null, null)); } @Override public final void writeStartObject() throws IOException { _verifyValueWrite("start an object"); _writeContext = _writeContext.createChildObjectContext(); Boolean style = _outputOptions.getDefaultFlowStyle().getStyleBoolean(); String yamlTag = _typeId; boolean implicit = (yamlTag == null); String anchor = _objectId; if (anchor != null) { _objectId = null; } _emitter.emit(new MappingStartEvent(anchor, yamlTag, implicit, null, null, style)); } @Override public final void writeEndObject() throws IOException { if (!_writeContext.inObject()) { _reportError("Current context not Object but "+_writeContext.typeDesc()); } // just to make sure we don't "leak" type ids _typeId = null; _writeContext = _writeContext.getParent(); _emitter.emit(new MappingEndEvent(null, null)); } /* /********************************************************** /* Output method implementations, textual /********************************************************** */ @Override public void writeString(String text) throws IOException,JsonGenerationException { if (text == null) { writeNull(); return; } _verifyValueWrite("write String value"); Character style = STYLE_QUOTED; if (Feature.MINIMIZE_QUOTES.enabledIn(_formatFeatures) && !isBooleanContent(text)) { // If this string could be interpreted as a number, it must be quoted. if (Feature.ALWAYS_QUOTE_NUMBERS_AS_STRINGS.enabledIn(_formatFeatures) && PLAIN_NUMBER_P.matcher(text).matches()) { style = STYLE_QUOTED; } else if (text.indexOf('\n') >= 0) { style = STYLE_LITERAL; } else { style = STYLE_PLAIN; } } else if (Feature.LITERAL_BLOCK_STYLE.enabledIn(_formatFeatures) && text.indexOf('\n') >= 0) { style = STYLE_LITERAL; } _writeScalar(text, "string", style); } private boolean isBooleanContent(String text) { return text.equals("true") || text.equals("false"); } @Override public void writeString(char[] text, int offset, int len) throws IOException { writeString(new String(text, offset, len)); } @Override public final void writeString(SerializableString sstr) throws IOException { writeString(sstr.toString()); } @Override public void writeRawUTF8String(byte[] text, int offset, int len) throws IOException { _reportUnsupportedOperation(); } @Override public final void writeUTF8String(byte[] text, int offset, int len) throws IOException { writeString(new String(text, offset, len, "UTF-8")); } /* /********************************************************** /* Output method implementations, unprocessed ("raw") /********************************************************** */ @Override public void writeRaw(String text) throws IOException { _reportUnsupportedOperation(); } @Override public void writeRaw(String text, int offset, int len) throws IOException { _reportUnsupportedOperation(); } @Override public void writeRaw(char[] text, int offset, int len) throws IOException { _reportUnsupportedOperation(); } @Override public void writeRaw(char c) throws IOException { _reportUnsupportedOperation(); } @Override public void writeRawValue(String text) throws IOException { _reportUnsupportedOperation(); } @Override public void writeRawValue(String text, int offset, int len) throws IOException { _reportUnsupportedOperation(); } @Override public void writeRawValue(char[] text, int offset, int len) throws IOException { _reportUnsupportedOperation(); } /* /********************************************************** /* Output method implementations, base64-encoded binary /********************************************************** */ @Override public void writeBinary(Base64Variant b64variant, byte[] data, int offset, int len) throws IOException { if (data == null) { writeNull(); return; } _verifyValueWrite("write Binary value"); if (offset > 0 || (offset+len) != data.length) { data = Arrays.copyOfRange(data, offset, offset+len); } _writeScalarBinary(b64variant, data); } /* /********************************************************** /* Output method implementations, scalars /********************************************************** */ @Override public void writeBoolean(boolean state) throws IOException { _verifyValueWrite("write boolean value"); _writeScalar(state ? "true" : "false", "bool", STYLE_SCALAR); } @Override public void writeNumber(int i) throws IOException { _verifyValueWrite("write number"); _writeScalar(String.valueOf(i), "int", STYLE_SCALAR); } @Override public void writeNumber(long l) throws IOException { // First: maybe 32 bits is enough? if (l <= MAX_INT_AS_LONG && l >= MIN_INT_AS_LONG) { writeNumber((int) l); return; } _verifyValueWrite("write number"); _writeScalar(String.valueOf(l), "long", STYLE_SCALAR); } @Override public void writeNumber(BigInteger v) throws IOException { if (v == null) { writeNull(); return; } _verifyValueWrite("write number"); _writeScalar(String.valueOf(v.toString()), "java.math.BigInteger", STYLE_SCALAR); } @Override public void writeNumber(double d) throws IOException { _verifyValueWrite("write number"); _writeScalar(String.valueOf(d), "double", STYLE_SCALAR); } @Override public void writeNumber(float f) throws IOException { _verifyValueWrite("write number"); _writeScalar(String.valueOf(f), "float", STYLE_SCALAR); } @Override public void writeNumber(BigDecimal dec) throws IOException { if (dec == null) { writeNull(); return; } _verifyValueWrite("write number"); String str = isEnabled(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN) ? dec.toPlainString() : dec.toString(); _writeScalar(str, "java.math.BigDecimal", STYLE_SCALAR); } @Override public void writeNumber(String encodedValue) throws IOException,JsonGenerationException, UnsupportedOperationException { if (encodedValue == null) { writeNull(); return; } _verifyValueWrite("write number"); _writeScalar(encodedValue, "number", STYLE_SCALAR); } @Override public void writeNull() throws IOException { _verifyValueWrite("write null value"); // no real type for this, is there? _writeScalar("null", "object", STYLE_SCALAR); } /* /********************************************************** /* Public API, write methods, Native Ids /********************************************************** */ @Override public boolean canWriteObjectId() { // yes, YAML does support Native Type Ids! // 10-Sep-2014, tatu: Except as per [#23] might not want to... return Feature.USE_NATIVE_OBJECT_ID.enabledIn(_formatFeatures); } @Override public boolean canWriteTypeId() { // yes, YAML does support Native Type Ids! // 10-Sep-2014, tatu: Except as per [#22] might not want to... return Feature.USE_NATIVE_TYPE_ID.enabledIn(_formatFeatures); } @Override public void writeTypeId(Object id) throws IOException { // should we verify there's no preceding type id? _typeId = String.valueOf(id); } @Override public void writeObjectRef(Object id) throws IOException { _verifyValueWrite("write Object reference"); AliasEvent evt = new AliasEvent(String.valueOf(id), null, null); _emitter.emit(evt); } @Override public void writeObjectId(Object id) throws IOException { // should we verify there's no preceding id? _objectId = String.valueOf(id); } /* /********************************************************** /* Implementations for methods from base class /********************************************************** */ @Override protected final void _verifyValueWrite(String typeMsg) throws IOException { int status = _writeContext.writeValue(); if (status == JsonWriteContext.STATUS_EXPECT_NAME) { _reportError("Can not "+typeMsg+", expecting field name"); } } @Override protected void _releaseBuffers() { // nothing special to do... } /* /********************************************************** /* Internal methods /********************************************************** */ // Implicit means that (type) tags won't be shown, right? private final static ImplicitTuple NO_TAGS = new ImplicitTuple(true, true); // ... and sometimes we specifically DO want explicit tag: private final static ImplicitTuple EXPLICIT_TAGS = new ImplicitTuple(false, false); protected void _writeScalar(String value, String type, Character style) throws IOException { _emitter.emit(_scalarEvent(value, style)); } private void _writeScalarBinary(Base64Variant b64variant, byte[] data) throws IOException { // 15-Dec-2017, tatu: as per [dataformats-text#62], can not use SnakeYAML's internal // codec. Also: force use of linefeed variant if using default if (b64variant == Base64Variants.getDefaultVariant()) { b64variant = Base64Variants.MIME; } String encoded = b64variant.encode(data); _emitter.emit(new ScalarEvent(null, TAG_BINARY, EXPLICIT_TAGS, encoded, null, null, STYLE_BASE64)); } protected ScalarEvent _scalarEvent(String value, Character style) { String yamlTag = _typeId; if (yamlTag != null) { _typeId = null; } String anchor = _objectId; if (anchor != null) { _objectId = null; } // 29-Nov-2017, tatu: Not 100% sure why we don't force explicit tags for // type id, but trying to do so seems to double up tag output... return new ScalarEvent(anchor, yamlTag, NO_TAGS, value, null, null, style); } }