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 Feature
s 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);
}
}