YAMLGenerator.java
package tools.jackson.dataformat.yaml;
import java.io.*;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Pattern;
import tools.jackson.core.*;
import tools.jackson.core.base.GeneratorBase;
import tools.jackson.core.io.IOContext;
import tools.jackson.core.io.NumberOutput;
import tools.jackson.core.json.DupDetector;
import tools.jackson.core.util.JacksonFeatureSet;
import tools.jackson.core.util.SimpleStreamWriteContext;
import tools.jackson.dataformat.yaml.util.StringQuotingChecker;
import org.snakeyaml.engine.v2.api.DumpSettings;
import org.snakeyaml.engine.v2.api.DumpSettingsBuilder;
import org.snakeyaml.engine.v2.comments.CommentType;
import org.snakeyaml.engine.v2.common.Anchor;
import org.snakeyaml.engine.v2.common.FlowStyle;
import org.snakeyaml.engine.v2.common.ScalarStyle;
import org.snakeyaml.engine.v2.common.SpecVersion;
import org.snakeyaml.engine.v2.emitter.Emitter;
import org.snakeyaml.engine.v2.events.*;
import org.snakeyaml.engine.v2.exceptions.YamlEngineException;
import org.snakeyaml.engine.v2.nodes.Tag;
import org.snakeyaml.engine.v2.resolver.JsonScalarResolver;
public class YAMLGenerator extends GeneratorBase
{
/*
/**********************************************************************
/* 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]*)?");
// @since 3.2
protected final static Pattern NEWLINE_SPLITTER_P = Pattern.compile("\r\n|\r|\n");
protected final static String TAG_BINARY = Tag.BINARY.toString();
// for property names, leave out quotes
private final static ScalarStyle STYLE_UNQUOTED_NAME = ScalarStyle.PLAIN;
// numbers, booleans, should use implicit
private final static ScalarStyle STYLE_SCALAR = ScalarStyle.PLAIN;
// Strings quoted for fun
private final static ScalarStyle STYLE_QUOTED = ScalarStyle.DOUBLE_QUOTED;
// Strings in literal (block) style
private final static ScalarStyle STYLE_LITERAL = ScalarStyle.LITERAL;
// Which flow style to use for Base64? Maybe basic quoted?
// 29-Nov-2017, tatu: Actually SnakeYAML uses block style so:
private final static ScalarStyle STYLE_BASE64 = STYLE_LITERAL;
private final static ScalarStyle STYLE_PLAIN = ScalarStyle.PLAIN;
/*
/**********************************************************************
/* Configuration
/**********************************************************************
*/
/**
* Bit flag composed of bits that indicate which {@link YAMLWriteFeature}s
* are enabled.
*/
protected int _formatWriteFeatures;
protected Writer _writer;
/**
* Caller-provided {@link OutputStream} that {@link #_writer} was constructed (by us)
* to wrap, if any; {@code null} if {@link #_writer} was handed to us by the caller.
*<p>
* A {@link Writer} we construct must always be flushed and closed: that is what
* hands its pending content over and returns its buffers to the recycler. It is
* shielded from this stream (see {@link tools.jackson.core.io.GuardedOutputStream}), which is instead
* flushed and closed directly, as per {@link StreamWriteFeature#FLUSH_PASSED_TO_STREAM}
* and {@link StreamWriteFeature#AUTO_CLOSE_TARGET}.
*
* @since 3.3
*/
protected final OutputStream _target;
protected DumpSettings _outputOptions;
protected final boolean _cfgMinimizeQuotes;
protected final SpecVersion _docVersion;
/*
/**********************************************************************
/* Output state
/**********************************************************************
*/
protected SimpleStreamWriteContext _streamWriteContext;
protected Emitter _emitter;
/**
* 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.
*/
protected String _typeId;
protected int _rootValueCount;
protected final StringQuotingChecker _quotingChecker;
/*
/**********************************************************************
/* Life-cycle
/**********************************************************************
*/
public YAMLGenerator(ObjectWriteContext writeContext, IOContext ioCtxt,
int streamWriteFeatures, int yamlFeatures,
StringQuotingChecker quotingChecker,
Writer out, SpecVersion version,
DumpSettings dumpOptions)
{
this(writeContext, ioCtxt, streamWriteFeatures, yamlFeatures, quotingChecker,
out, null, version, dumpOptions);
}
/**
* @param out Writer to write to: either provided by the caller, or constructed
* by Jackson to wrap {@code target}
* @param target Caller-provided stream that {@code out} wraps, if {@code out} was
* constructed by Jackson (and hence must always be flushed and closed);
* {@code null} if {@code out} was provided by the caller
*
* @since 3.3
*/
public YAMLGenerator(ObjectWriteContext writeContext, IOContext ioCtxt,
int streamWriteFeatures, int yamlFeatures,
StringQuotingChecker quotingChecker,
Writer out, OutputStream target, SpecVersion version,
DumpSettings dumpOptions)
{
super(writeContext, ioCtxt, streamWriteFeatures);
_target = target;
final DupDetector dups = StreamWriteFeature.STRICT_DUPLICATE_DETECTION.enabledIn(streamWriteFeatures)
? DupDetector.rootDetector(this) : null;
_streamWriteContext = SimpleStreamWriteContext.createRootContext(dups);
_formatWriteFeatures = yamlFeatures;
_cfgMinimizeQuotes = YAMLWriteFeature.MINIMIZE_QUOTES.enabledIn(_formatWriteFeatures);
_quotingChecker = quotingChecker;
_writer = out;
_docVersion = version;
if (dumpOptions == null) {
dumpOptions = buildDumperOptions(streamWriteFeatures, yamlFeatures, version);
}
_outputOptions = dumpOptions;
_emitter = new Emitter(_outputOptions, new WriterWrapper(_writer));
// should we start output now, or try to defer?
_emit(new StreamStartEvent());
_emitStartDocument();
}
protected DumpSettings buildDumperOptions(int streamWriteFeatures, int yamlFeatures,
SpecVersion version)
{
DumpSettingsBuilder opt = DumpSettings.builder();
// would we want canonical?
if (YAMLWriteFeature.CANONICAL_OUTPUT.enabledIn(_formatWriteFeatures)) {
opt.setCanonical(true);
} else {
opt.setCanonical(false);
// if not, MUST specify flow styles
opt.setDefaultFlowStyle(FlowStyle.BLOCK);
}
// split-lines for text blocks?
opt.setSplitLines(YAMLWriteFeature.SPLIT_LINES.enabledIn(_formatWriteFeatures));
// array indentation?
if (YAMLWriteFeature.INDENT_ARRAYS.enabledIn(_formatWriteFeatures)) {
// 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);
}
// [dataformats-text#175]: further configurability that overrides prev setting
if (YAMLWriteFeature.INDENT_ARRAYS_WITH_INDICATOR.enabledIn(_formatWriteFeatures)) {
opt.setIndicatorIndent(2);
opt.setIndentWithIndicator(true);
}
if (YAMLWriteFeature.ALLOW_LONG_KEYS.enabledIn(_formatWriteFeatures)) {
opt.setMaxSimpleKeyLength(1024);
}
// 03-Oct-2020, tatu: Specify spec version; however, does not seem to make
// any difference?
opt.setYamlDirective(Optional.ofNullable(version));
// Enable comment output support (needed for writeComment())
opt.setDumpComments(true);
return opt.build();
}
/*
/**********************************************************************
/* Versioned
/**********************************************************************
*/
@Override
public Version version() {
return PackageVersion.VERSION;
}
/*
/**********************************************************************
/* Overrides: capability introspection
/**********************************************************************
*/
@Override // @since 3.2
public boolean canWriteComments() {
// 06-Apr-2026, tatu: as per [dataformats-text#36]:
return true;
}
@Override
public boolean canWriteObjectId() {
// yes, YAML does support Native Type Ids!
// 10-Sep-2014, tatu: Except as per [#23] might not want to...
return YAMLWriteFeature.USE_NATIVE_OBJECT_ID.enabledIn(_formatWriteFeatures);
}
@Override
public boolean canWriteTypeId() {
// yes, YAML does support Native Type Ids!
// 10-Sep-2014, tatu: Except as per [#22] might not want to...
return YAMLWriteFeature.USE_NATIVE_TYPE_ID.enabledIn(_formatWriteFeatures);
}
@Override
public boolean has(StreamWriteCapability capability) {
return DEFAULT_TEXTUAL_WRITE_CAPABILITIES.isEnabled(capability);
}
@Override
public JacksonFeatureSet<StreamWriteCapability> streamWriteCapabilities() {
return DEFAULT_TEXTUAL_WRITE_CAPABILITIES;
}
/*
/**********************************************************************
/* Overridden output state handling methods
/**********************************************************************
*/
@Override
public final TokenStreamContext streamWriteContext() { return _streamWriteContext; }
@Override
public final Object currentValue() {
return _streamWriteContext.currentValue();
}
@Override
public final void assignCurrentValue(Object v) {
_streamWriteContext.assignCurrentValue(v);
}
/*
/**********************************************************************
/* Overridden methods, configuration
/**********************************************************************
*/
@Override
public Object streamWriteOutputTarget() {
// [dataformats-text#749]: Writer we constructed ourselves is an implementation
// detail, shielded from caller's stream: so expose the stream instead
return (_target != null) ? _target : _writer;
}
/**
* SnakeYAML does not expose buffered content amount, so we can only return
* <code>-1</code> from here
*/
@Override
public int streamWriteOutputBuffered() {
return -1;
}
/*
/**********************************************************************
/* Overrides: config access
/**********************************************************************
*/
@Override
public PrettyPrinter getPrettyPrinter() {
return null;
}
/*
/**********************************************************************
/* Extended API, configuration
/**********************************************************************
*/
public final boolean isEnabled(YAMLWriteFeature f) {
return (_formatWriteFeatures & f.getMask()) != 0;
}
/*
/**********************************************************************
/* Overridden methods; writing property names
/**********************************************************************
*/
// And then methods overridden to make final, streamline some aspects...
@Override
public JsonGenerator writeName(String name) throws JacksonException
{
if (!_streamWriteContext.writeName(name)) {
_reportError("Cannot write a property name, expecting a value");
}
_writeFieldName(name);
return this;
}
@Override
public JsonGenerator writeName(SerializableString name)
throws JacksonException
{
// Object is a value, need to verify it's allowed
if (!_streamWriteContext.writeName(name.getValue())) {
_reportError("Cannotwrite a property name, expecting a value");
}
_writeFieldName(name.getValue());
return this;
}
@Override
public JsonGenerator writePropertyId(long id) throws JacksonException {
// 24-Jul-2019, tatu: Should not force construction of a String here...
String idStr = Long.toString(id);
if (!_streamWriteContext.writeName(idStr)) {
_reportError("Cannot write a property id, expecting a value");
}
// to avoid quoting
// _writeFieldName(idStr);
_writeScalar(idStr, "int", STYLE_SCALAR);
return this;
}
private final void _writeFieldName(String name) throws JacksonException
{
_writeScalar(name, "string",
_quotingChecker.needToQuoteName(name) ? STYLE_QUOTED : STYLE_UNQUOTED_NAME);
}
/*
/**********************************************************************
/* Public API: low-level I/O
/**********************************************************************
*/
@Override
public final void flush()
{
// [dataformats-text#749]: nothing to flush once closed -- and target may
// have been closed along with us
if (isClosed()) {
return;
}
try {
if (_target != null) {
// [dataformats-text#749]: a Writer we constructed ourselves is just a
// buffer, so must be flushed regardless (caller's stream is shielded)
_writer.flush();
}
if (isEnabled(StreamWriteFeature.FLUSH_PASSED_TO_STREAM)) {
final Flushable target = (_target != null) ? _target : _writer;
target.flush();
}
} catch (IOException e) {
throw _wrapIOFailure(e);
}
}
@Override
public void close()
{
if (!isClosed()) {
// 11-Dec-2019, tatu: Should perhaps check if content is to be auto-closed...
// but need END_DOCUMENT regardless
try {
_emitEndDocument();
_emit(new StreamEndEvent());
} catch (Throwable t) {
// [dataformats-text#752]: must close regardless, but without masking
// the original failure
try {
super.close();
} catch (Throwable t2) {
t.addSuppressed(t2);
}
throw t;
}
super.close();
}
}
@Override
protected void _closeInput() throws IOException
{
if (_writer == null) {
return;
}
final boolean closeTarget = _ioContext.isResourceManaged()
|| isEnabled(StreamWriteFeature.AUTO_CLOSE_TARGET);
if (_target == null) {
// 25-Nov-2008, tatus: As per [JACKSON-16] we are not to call close()
// on the underlying Writer, unless we "own" it, or auto-closing
// feature is enabled.
if (closeTarget) {
_writer.close();
} else if (isEnabled(StreamWriteFeature.FLUSH_PASSED_TO_STREAM)) {
// If we can't close it, we should at least flush
_writer.flush();
}
return;
}
// [dataformats-text#749]: a Writer we constructed ourselves must be closed
// regardless: that hands its buffered content over to the caller's
// OutputStream and recycles its buffer. Caller's stream is shielded from
// that, and closed (or flushed) as per features enabled now --
// [dataformats-text#752] even if closing our Writer fails: try-with-resources
// ensures that, and reports the first failure (with any later one added
// as suppressed)
final Closeable target = closeTarget ? _target
: (isEnabled(StreamWriteFeature.FLUSH_PASSED_TO_STREAM) ? _target::flush : null);
try (target) {
_writer.close();
}
}
/*
/**********************************************************************
/* Public API: structural output
/**********************************************************************
*/
@Override
public JsonGenerator writeStartArray() throws JacksonException
{
_verifyValueWrite("start an array");
_streamWriteContext = _streamWriteContext.createChildArrayContext(null);
_streamWriteConstraints.validateNestingDepth(_streamWriteContext.getNestingDepth());
FlowStyle style = _outputOptions.getDefaultFlowStyle();
String yamlTag = _typeId;
boolean implicit = (yamlTag == null);
Optional<Anchor> anchor = Optional.ofNullable(_objectId).map(Anchor::new);
_objectId = null;
_emit(new SequenceStartEvent(anchor, Optional.ofNullable(yamlTag),
implicit, style));
return this;
}
@Override
public JsonGenerator writeStartArray(Object currValue) throws JacksonException {
writeStartArray();
assignCurrentValue(currValue);
return this;
}
@Override
public JsonGenerator writeEndArray() throws JacksonException
{
if (!_streamWriteContext.inArray()) {
_reportError("Current context not Array but "+_streamWriteContext.typeDesc());
}
// just to make sure we don't "leak" type ids
_typeId = null;
_streamWriteContext = _streamWriteContext.getParent();
_emit(new SequenceEndEvent());
return this;
}
@Override
public JsonGenerator writeStartObject() throws JacksonException
{
_verifyValueWrite("start an object");
_streamWriteContext = _streamWriteContext.createChildObjectContext(null);
_streamWriteConstraints.validateNestingDepth(_streamWriteContext.getNestingDepth());
FlowStyle style = _outputOptions.getDefaultFlowStyle();
String yamlTag = _typeId;
boolean implicit = (yamlTag == null);
Optional<Anchor> anchor = Optional.ofNullable(_objectId).map(Anchor::new);
_objectId = null;
_emit(new MappingStartEvent(anchor, Optional.ofNullable(yamlTag), implicit, style));
return this;
}
@Override
public JsonGenerator writeStartObject(Object currValue) throws JacksonException {
writeStartObject();
assignCurrentValue(currValue);
return this;
}
@Override
public JsonGenerator writeEndObject() throws JacksonException
{
if (!_streamWriteContext.inObject()) {
_reportError("Current context not Object but "+_streamWriteContext.typeDesc());
}
// just to make sure we don't "leak" type ids
_typeId = null;
_streamWriteContext = _streamWriteContext.getParent();
_emit(new MappingEndEvent());
return this;
}
/*
/**********************************************************************
/* Output method implementations, textual
/**********************************************************************
*/
@Override
public JsonGenerator writeString(String text) throws JacksonException
{
if (text == null) {
return writeNull();
}
_verifyValueWrite("write String value");
// [dataformats-text#50]: Empty String always quoted
if (text.isEmpty()) {
_writeScalar(text, "string", STYLE_QUOTED);
return this;
}
ScalarStyle style;
if (_cfgMinimizeQuotes) {
if (text.indexOf('\n') >= 0) {
style = STYLE_LITERAL;
// If one of reserved values ("true", "null"), or, number, preserve quoting:
} else if (_quotingChecker.needToQuoteValue(text)
|| (YAMLWriteFeature.ALWAYS_QUOTE_NUMBERS_AS_STRINGS.enabledIn(_formatWriteFeatures)
&& _looksLikeYAMLNumber(text))
) {
style = STYLE_QUOTED;
} else {
style = STYLE_PLAIN;
}
} else {
if (YAMLWriteFeature.LITERAL_BLOCK_STYLE.enabledIn(_formatWriteFeatures)
&& text.indexOf('\n') >= 0) {
style = STYLE_LITERAL;
} else {
style = STYLE_QUOTED;
}
}
_writeScalar(text, "string", style);
return this;
}
@Override
public JsonGenerator writeString(char[] text, int offset, int len) throws JacksonException
{
return writeString(new String(text, offset, len));
}
/**
* Checks whether given String value would be re-read as a YAML number if emitted
* unquoted; used by {@link YAMLWriteFeature#ALWAYS_QUOTE_NUMBERS_AS_STRINGS}.
* Combines the historical {@link #PLAIN_NUMBER_P} check (retained for backwards
* compatibility) with the implicit resolver patterns
* ({@link JsonScalarResolver#INT}, {@link JsonScalarResolver#FLOAT}) that
* {@link YAMLParser} uses to resolve plain scalars back to numbers, so that
* exponent ({@code 1e5}) and non-finite ({@code .inf}, {@code .nan}) forms, which
* {@link #PLAIN_NUMBER_P} does not match, are quoted too and thus round-trip.
* See [dataformats-text#701].
*
* @since 3.1.6
*/
protected boolean _looksLikeYAMLNumber(String text) {
// 23-Jul-2026, tatu: Regexps are relatively costly so avoid them for the
// common case of "regular" text: all forms matched below have to start
// with one of following characters
if (text.isEmpty()) {
return false;
}
switch (text.charAt(0)) {
case '+': case '-': case '.':
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
break;
default:
return false;
}
// NOTE: unlike YAML 1.1 (used by 2.x), the YAML 1.2 JSON schema resolves
// hex ("0x1F"), binary ("0b101"), underscore ("12_34") and 60-base ("1:30")
// forms as Strings already -- so, unlike 2.x, no exclusions needed here
return PLAIN_NUMBER_P.matcher(text).matches()
|| JsonScalarResolver.INT.matcher(text).matches()
|| JsonScalarResolver.FLOAT.matcher(text).matches();
}
@Override
public JsonGenerator writeString(SerializableString sstr)
throws JacksonException
{
return writeString(sstr.toString());
}
@Override
public JsonGenerator writeRawUTF8String(byte[] text, int offset, int len)
throws JacksonException
{
return _reportUnsupportedOperation();
}
@Override
public JsonGenerator writeUTF8String(byte[] text, int offset, int len)
throws JacksonException
{
return writeString(new String(text, offset, len, StandardCharsets.UTF_8));
}
/*
/**********************************************************************
/* Output method implementations, unprocessed ("raw")
/**********************************************************************
*/
@Override
public JsonGenerator writeRaw(String text) throws JacksonException {
return _reportUnsupportedOperation();
}
@Override
public JsonGenerator writeRaw(String text, int offset, int len) throws JacksonException {
return _reportUnsupportedOperation();
}
@Override
public JsonGenerator writeRaw(char[] text, int offset, int len) throws JacksonException {
return _reportUnsupportedOperation();
}
@Override
public JsonGenerator writeRaw(char c) throws JacksonException {
return _reportUnsupportedOperation();
}
@Override
public JsonGenerator writeRawValue(String text) throws JacksonException {
return _reportUnsupportedOperation();
}
@Override
public JsonGenerator writeRawValue(String text, int offset, int len) throws JacksonException {
return _reportUnsupportedOperation();
}
@Override
public JsonGenerator writeRawValue(char[] text, int offset, int len) throws JacksonException {
return _reportUnsupportedOperation();
}
/*
/**********************************************************************
/* Output method implementations, base64-encoded binary
/**********************************************************************
*/
@Override
public JsonGenerator writeBinary(Base64Variant b64variant, byte[] data, int offset, int len) throws JacksonException
{
if (data == null) {
return writeNull();
}
_verifyValueWrite("write Binary value");
if (offset > 0 || (offset+len) != data.length) {
data = Arrays.copyOfRange(data, offset, offset+len);
}
_writeScalarBinary(b64variant, data);
return this;
}
/*
/**********************************************************************
/* Output method implementations, scalars
/**********************************************************************
*/
@Override
public JsonGenerator writeBoolean(boolean state) throws JacksonException
{
_verifyValueWrite("write boolean value");
_writeScalar(state ? "true" : "false", "bool", STYLE_SCALAR);
return this;
}
@Override
public JsonGenerator writeNumber(short v) throws JacksonException {
return writeNumber((int) v);
}
@Override
public JsonGenerator writeNumber(int v) throws JacksonException
{
_verifyValueWrite("write number");
_writeScalar(Integer.toString(v), "int", STYLE_SCALAR);
return this;
}
@Override
public JsonGenerator writeNumber(long l) throws JacksonException
{
// First: maybe 32 bits is enough?
if (l <= MAX_INT_AS_LONG && l >= MIN_INT_AS_LONG) {
return writeNumber((int) l);
}
_verifyValueWrite("write number");
_writeScalar(Long.toString(l), "long", STYLE_SCALAR);
return this;
}
@Override
public JsonGenerator writeNumber(BigInteger v) throws JacksonException
{
if (v == null) {
return writeNull();
}
_verifyValueWrite("write number");
_writeScalar(v.toString(), "java.math.BigInteger", STYLE_SCALAR);
return this;
}
@Override
public JsonGenerator writeNumber(double d) throws JacksonException
{
_verifyValueWrite("write number");
if (NumberOutput.notFinite(d) && isEnabled(YAMLWriteFeature.USE_YAML_NONFINITE_NOTATION)) {
if (Double.isNaN(d)) {
_writeScalar(".nan", "double", STYLE_PLAIN);
} else if (d > 0.0) {
_writeScalar(".inf", "double", STYLE_PLAIN);
} else {
_writeScalar("-.inf", "double", STYLE_PLAIN);
}
return this;
}
_writeScalar(NumberOutput.toString(d, isEnabled(StreamWriteFeature.USE_FAST_DOUBLE_WRITER)),
"double", STYLE_SCALAR);
return this;
}
@Override
public JsonGenerator writeNumber(float f) throws JacksonException
{
_verifyValueWrite("write number");
if (NumberOutput.notFinite(f) && isEnabled(YAMLWriteFeature.USE_YAML_NONFINITE_NOTATION)) {
if (Float.isNaN(f)) {
_writeScalar(".nan", "float", STYLE_PLAIN);
} else if (f > 0.0f) {
_writeScalar(".inf", "float", STYLE_PLAIN);
} else {
_writeScalar("-.inf", "float", STYLE_PLAIN);
}
return this;
}
_writeScalar(NumberOutput.toString(f, isEnabled(StreamWriteFeature.USE_FAST_DOUBLE_WRITER)),
"float", STYLE_SCALAR);
return this;
}
@Override
public JsonGenerator writeNumber(BigDecimal dec) throws JacksonException
{
if (dec == null) {
return writeNull();
}
_verifyValueWrite("write number");
String str = isEnabled(StreamWriteFeature.WRITE_BIGDECIMAL_AS_PLAIN) ? dec.toPlainString() : dec.toString();
_writeScalar(str, "java.math.BigDecimal", STYLE_SCALAR);
return this;
}
@Override
public JsonGenerator writeNumber(String encodedValue) throws JacksonException
{
if (encodedValue == null) {
return writeNull();
}
_verifyValueWrite("write number");
_writeScalar(encodedValue, "number", STYLE_SCALAR);
return this;
}
@Override
public JsonGenerator writeNull() throws JacksonException
{
_verifyValueWrite("write null value");
// no real type for this, is there?
_writeScalar("null", "object", STYLE_SCALAR);
return this;
}
/*
/**********************************************************************
/* Public API, write methods, Native Ids
/**********************************************************************
*/
@Override
public JsonGenerator writeTypeId(Object id)
throws JacksonException
{
// should we verify there's no preceding type id?
_typeId = String.valueOf(id);
return this;
}
@Override
public JsonGenerator writeObjectRef(Object id)
throws JacksonException
{
_verifyValueWrite("write Object reference");
// NOTE: `String.valueOf()` to retain legacy handling of `null` id as "null"
// (`AliasEvent` itself does not allow missing Anchor)
AliasEvent evt = new AliasEvent(Optional.of(new Anchor(String.valueOf(id))));
_emit(evt);
return this;
}
@Override
public JsonGenerator writeObjectId(Object id)
throws JacksonException
{
// should we verify there's no preceding id?
_objectId = (id == null) ? null : String.valueOf(id);
return this;
}
/*
/**********************************************************************
/* Output method implementations, other
/**********************************************************************
*/
/**
* Method for writing a YAML comment.
* If called right after {@link #writeName}, the comment is emitted as an
* end-of-line (inline) comment on the property name line; otherwise it is
* emitted as a block comment (full line starting with {@code #}).
*<p>
* If {@code text} is {@code null}, an empty (blank) line is emitted instead.
*<p>
* Multi-line comments are supported: if the text contains newlines,
* each line will be emitted as a separate comment line.
*<p>
* Note: YAML comments are presentation detail and are not part of the
* serialization model per the YAML specification.
*
* @param text Comment text to write (without the leading {@code #});
* if {@code null}, writes an empty line
*
* @return This generator, to allow call chaining
*
* @throws JacksonException if there is a problem emitting the comment
*
* @since 3.2
*/
@Override
public YAMLGenerator writeComment(String text) throws JacksonException
{
if (text == null) {
_emit(new CommentEvent(CommentType.BLANK_LINE, "",
Optional.empty(), Optional.empty()));
return this;
}
// If we just wrote a property name (expecting value next),
// emit as an inline (end-of-line) comment; otherwise as a block comment
final CommentType type = (_streamWriteContext.inObject()
&& _streamWriteContext.hasCurrentName())
? CommentType.IN_LINE : CommentType.BLOCK;
// Split multi-line comments into separate CommentEvents;
// handle all standard linefeeds: "\r\n", "\r", "\n"
String[] lines = NEWLINE_SPLITTER_P.split(text, -1);
for (String line : lines) {
_emit(new CommentEvent(type, line,
Optional.empty(), Optional.empty()));
}
return this;
}
/*
/**********************************************************************
/* Implementations for methods from base class
/**********************************************************************
*/
@Override
protected final void _verifyValueWrite(String typeMsg) throws JacksonException
{
if (!_streamWriteContext.writeValue()) {
_reportError("Cannot "+typeMsg+", expecting a property name");
}
if (_streamWriteContext.inRoot()) {
// Start-doc emitted when creating generator, but otherwise need it; similarly,
// need matching end-document to close earlier open one
if (_streamWriteContext.getCurrentIndex() > 0) {
_emitEndDocument();
_emitStartDocument();
}
}
}
@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, ScalarStyle style) throws JacksonException
{
_emit(_scalarEvent(value, style));
}
private void _writeScalarBinary(Base64Variant b64variant,
byte[] data) throws JacksonException
{
// 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;
}
final String lf = _lf();
String encoded = b64variant.encode(data, false, lf);
_emit(new ScalarEvent(Optional.empty(), Optional.ofNullable(TAG_BINARY), EXPLICIT_TAGS, encoded, STYLE_BASE64));
}
protected ScalarEvent _scalarEvent(String value, ScalarStyle style)
{
String yamlTag = _typeId;
if (yamlTag != null) {
_typeId = null;
}
Optional<Anchor> anchor = Optional.ofNullable(_objectId).map(Anchor::new);
_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, Optional.ofNullable(yamlTag), NO_TAGS, value, style);
}
protected String _lf() {
return _outputOptions.getBestLineBreak();
}
protected void _emitStartDocument() throws JacksonException
{
Map<String,String> noTags = Map.of();
boolean startMarker = YAMLWriteFeature.WRITE_DOC_START_MARKER.enabledIn(_formatWriteFeatures);
_emit(new DocumentStartEvent(startMarker, _outputOptions.getYamlDirective(),
// for 1.10 was: ((version == null) ? null : version.getArray()),
noTags));
}
protected void _emitEndDocument() throws JacksonException {
_emit(new DocumentEndEvent(false));
}
protected final void _emit(Event e) throws JacksonException {
try {
_emitter.emit(e);
} catch (UncheckedIOException ex) {
// throws `JacksonIOException`:
throw _wrapIOFailure(ex.getCause());
} catch (YamlEngineException ex) {
// Wrap SnakeYAML exception as Jackson exception
throw new JacksonYAMLWriteException(this, ex.getMessage(), ex);
}
}
}