org.diorite.config.serialization.snakeyaml.emitter.Emitter.java Source code

Java tutorial

Introduction

Here is the source code for org.diorite.config.serialization.snakeyaml.emitter.Emitter.java

Source

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2016. Diorite (by Bartomiej Mazur (aka GotoFinal))
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.diorite.config.serialization.snakeyaml.emitter;

import javax.annotation.Nullable;

import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.yaml.snakeyaml.DumperOptions.Version;
import org.yaml.snakeyaml.emitter.Emitable;
import org.yaml.snakeyaml.emitter.EmitterException;
import org.yaml.snakeyaml.emitter.ScalarAnalysis;
import org.yaml.snakeyaml.error.YAMLException;
import org.yaml.snakeyaml.events.AliasEvent;
import org.yaml.snakeyaml.events.CollectionEndEvent;
import org.yaml.snakeyaml.events.CollectionStartEvent;
import org.yaml.snakeyaml.events.DocumentEndEvent;
import org.yaml.snakeyaml.events.DocumentStartEvent;
import org.yaml.snakeyaml.events.Event;
import org.yaml.snakeyaml.events.ImplicitTuple;
import org.yaml.snakeyaml.events.MappingEndEvent;
import org.yaml.snakeyaml.events.MappingStartEvent;
import org.yaml.snakeyaml.events.NodeEvent;
import org.yaml.snakeyaml.events.ScalarEvent;
import org.yaml.snakeyaml.events.SequenceEndEvent;
import org.yaml.snakeyaml.events.SequenceStartEvent;
import org.yaml.snakeyaml.events.StreamEndEvent;
import org.yaml.snakeyaml.nodes.Tag;
import org.yaml.snakeyaml.reader.StreamReader;
import org.yaml.snakeyaml.scanner.Constant;
import org.yaml.snakeyaml.util.ArrayStack;

import org.diorite.commons.math.DioriteMathUtils;
import org.diorite.config.serialization.Serialization;
import org.diorite.config.serialization.snakeyaml.DumperOptions;

/**
 * <pre>
 * Emitter expects events obeying the following grammar:
 * stream ::= STREAM-START document* STREAM-END
 * document ::= DOCUMENT-START node DOCUMENT-END
 * node ::= SCALAR | sequence | mapping
 * sequence ::= SEQUENCE-START node* SEQUENCE-END
 * mapping ::= MAPPING-START (node node)* MAPPING-END
 * </pre>
 */
public final class Emitter implements Emitable {
    private static final Map<Character, String> ESCAPE_REPLACEMENTS = new HashMap<>(15);
    public static final int MIN_INDENT = 1;
    public static final int MAX_INDENT = 10;

    private static final char[] SPACE = { ' ' };
    public static final int BEST_WIDTH = 80;
    public static final int SMALL_LENGTH = 128;
    public static final int HEX_RADIX = 16;

    static {
        ESCAPE_REPLACEMENTS.put('\0', "0");
        ESCAPE_REPLACEMENTS.put('\u0007', "a");
        ESCAPE_REPLACEMENTS.put('\u0008', "b");
        ESCAPE_REPLACEMENTS.put('\u0009', "t");
        ESCAPE_REPLACEMENTS.put('\n', "n");
        ESCAPE_REPLACEMENTS.put('\u000B', "v");
        ESCAPE_REPLACEMENTS.put('\u000C', "f");
        ESCAPE_REPLACEMENTS.put('\r', "r");
        ESCAPE_REPLACEMENTS.put('\u001B', "e");
        ESCAPE_REPLACEMENTS.put('"', "\"");
        ESCAPE_REPLACEMENTS.put('\\', "\\");
        ESCAPE_REPLACEMENTS.put('\u0085', "N");
        ESCAPE_REPLACEMENTS.put('\u00A0', "_");
        ESCAPE_REPLACEMENTS.put('\u2028', "L");
        ESCAPE_REPLACEMENTS.put('\u2029', "P");
    }

    static final Map<String, String> DEFAULT_TAG_PREFIXES = new LinkedHashMap<>(2);

    static {
        DEFAULT_TAG_PREFIXES.put("!", "!");
        DEFAULT_TAG_PREFIXES.put(Tag.PREFIX, "!!");
    }

    private final Serialization serialization;

    // The stream should have the methods `write` and possibly `flush`.
    private final Writer stream;

    // Encoding is defined by Writer (cannot be overriden by STREAM-START.)
    // private Charset encoding;

    // Emitter is a state machine with a stack of states to handle nested
    // structures.
    final ArrayStack<EmitterState> states;
    EmitterState state;

    // Current event and the event queue.
    private final Queue<Event> events;
    @Nullable
    Event event;

    // The current indentation level and the stack of previous indents.
    final ArrayStack<Integer> indents;
    @Nullable
    Integer indent;

    // Flow level.
    int flowLevel;

    // Contexts.
    private boolean rootContext;
    private boolean mappingContext;
    private boolean simpleKeyContext;

    //
    // Characteristics of the last emitted character:
    // - current position.
    // - is it a whitespace?
    // - is it an indention character
    // (indentation space, '-', '?', or ':')?
    // private int line; this variable is not used
    int column;
    private boolean whitespace;
    private boolean indention;
    boolean openEnded;

    // Formatting details.
    Boolean canonical;
    // pretty print flow by adding extra line breaks
    Boolean prettyFlow;

    private boolean allowUnicode;
    private int bestIndent;
    int indicatorIndent;
    int bestWidth;
    private char[] bestLineBreak;
    boolean splitLines;
    private int longCommentThreshold;
    private String longCommentBorder;
    private boolean longCommentBorderStartsWithComment;
    private String longCommentRightBorder;

    // Tag prefixes.
    Map<String, String> tagPrefixes;

    // Prepared anchor and tag.
    @Nullable
    private String preparedAnchor;
    @Nullable
    private String preparedTag;

    // Scalar analysis and style.
    @Nullable
    private ScalarAnalysis analysis;
    @Nullable
    private Character style;

    public Emitter(Serialization serialization, Writer stream, DumperOptions opts) {
        this.serialization = serialization;
        // The stream should have the methods `write` and possibly `flush`.
        this.stream = stream;
        // Emitter is a state machine with a stack of states to handle nested
        // structures.
        this.states = new ArrayStack<>(100);
        this.state = new ExpectStreamStart();
        // Current event and the event queue.
        this.events = new ArrayBlockingQueue<>(100);
        this.event = null;
        // The current indentation level and the stack of previous indents.
        this.indents = new ArrayStack<>(10);
        this.indent = null;
        // Flow level.
        this.flowLevel = 0;
        // Contexts.
        this.mappingContext = false;
        this.simpleKeyContext = false;

        //
        // Characteristics of the last emitted character:
        // - current position.
        // - is it a whitespace?
        // - is it an indention character
        // (indentation space, '-', '?', or ':')?
        this.column = 0;
        this.whitespace = true;
        this.indention = true;

        // Whether the document requires an explicit document indicator
        this.openEnded = false;

        // Formatting details.
        this.canonical = opts.isCanonical();
        this.prettyFlow = opts.isPrettyFlow();
        this.allowUnicode = opts.isAllowUnicode();
        this.bestIndent = 2;
        if ((opts.getIndent() > MIN_INDENT) && (opts.getIndent() < MAX_INDENT)) {
            this.bestIndent = opts.getIndent();
        }
        this.indicatorIndent = opts.getIndicatorIndent();
        this.bestWidth = BEST_WIDTH;
        if (opts.getWidth() > (this.bestIndent * 2)) {
            this.bestWidth = opts.getWidth();
        }
        this.bestLineBreak = opts.getLineBreak().getString().toCharArray();
        this.splitLines = opts.getSplitLines();
        this.longCommentThreshold = opts.getLongCommentThreshold();
        this.longCommentBorder = opts.getLongCommentBorder();
        this.longCommentBorderStartsWithComment = !this.longCommentBorder.isEmpty()
                && (this.longCommentBorder.charAt(0) == '#');
        this.longCommentRightBorder = opts.getLongCommentRightBorder();

        // Tag prefixes.
        this.tagPrefixes = new LinkedHashMap<>(10);

        // Prepared anchor and tag.
        this.preparedAnchor = null;
        this.preparedTag = null;

        // Scalar analysis and style.
        this.analysis = null;
        this.style = null;
    }

    @Override
    public void emit(Event event) throws IOException {
        this.events.add(event);
        while (!this.needMoreEvents()) {
            this.event = this.events.poll();
            this.state.expect(this);
            this.event = null;
        }
    }

    // In some cases, we wait for a few next events before emitting.

    private boolean needMoreEvents() {
        if (this.events.isEmpty()) {
            return true;
        }
        Event event = this.events.peek();
        if (event instanceof DocumentStartEvent) {
            return this.needEvents(1);
        } else if (event instanceof SequenceStartEvent) {
            return this.needEvents(2);
        } else if (event instanceof MappingStartEvent) {
            return this.needEvents(3);
        } else {
            return false;
        }
    }

    private boolean needEvents(int count) {
        int level = 0;
        Iterator<Event> iter = this.events.iterator();
        iter.next();
        while (iter.hasNext()) {
            Event event = iter.next();
            if ((event instanceof DocumentStartEvent) || (event instanceof CollectionStartEvent)) {
                level++;
            } else if ((event instanceof DocumentEndEvent) || (event instanceof CollectionEndEvent)) {
                level--;
            } else if (event instanceof StreamEndEvent) {
                level = -1;
            }
            if (level < 0) {
                return false;
            }
        }
        return this.events.size() < (count + 1);
    }

    private void increaseIndent(boolean flow, boolean indentLess) {
        this.indents.push(this.indent);
        if (this.indent == null) {
            if (flow) {
                this.indent = this.bestIndent;
            } else {
                this.indent = 0;
            }
        } else if (!indentLess) {
            this.indent += this.bestIndent;
        }
    }

    void expectNode(boolean root, boolean mapping, boolean simpleKey) throws IOException {
        this.expectNode(root, mapping, simpleKey, this.indent);
    }

    void expectNode(boolean root, boolean mapping, boolean simpleKey, @Nullable Integer lastIndent)
            throws IOException {
        this.rootContext = root;
        this.mappingContext = mapping;
        this.simpleKeyContext = simpleKey;
        if (this.event instanceof AliasEvent) {
            this.expectAlias();
        } else if ((this.event instanceof ScalarEvent) || (this.event instanceof CollectionStartEvent)) {
            this.processAnchor("&");
            this.processTag();
            if (this.event instanceof ScalarEvent) {
                this.expectScalar(lastIndent);
            } else if (this.event instanceof SequenceStartEvent) {
                if ((this.flowLevel != 0) || this.canonical || ((SequenceStartEvent) this.event).getFlowStyle()
                        || this.checkEmptySequence()) {
                    this.expectFlowSequence();
                } else {
                    this.expectBlockSequence();
                }
            } else {// MappingStartEvent
                if ((this.flowLevel != 0) || this.canonical || ((MappingStartEvent) this.event).getFlowStyle()
                        || this.checkEmptyMapping()) {
                    this.expectFlowMapping();
                } else {
                    this.expectBlockMapping();
                }
            }
        } else {
            throw new EmitterException("expected NodeEvent, but got " + this.event);
        }
    }

    private void expectAlias() throws IOException {
        assert this.event != null;
        if (((NodeEvent) this.event).getAnchor() == null) {
            throw new EmitterException("anchor is not specified for alias");
        }
        this.processAnchor("*");
        this.state = this.states.pop();
    }

    private void expectScalar(@Nullable Integer lastIndent) throws IOException {
        assert this.event != null;
        ScalarEvent ev = (ScalarEvent) this.event;

        ImplicitTuple implicitTuple = ev.getImplicit();
        if (implicitTuple instanceof ImplicitTupleExtension) {
            this.writeCommentSafe(((ImplicitTupleExtension) implicitTuple).getComment(), 1, 1);
        }
        this.writeIndentForce(lastIndent);

        this.increaseIndent(true, false);
        this.processScalar();
        this.indent = this.indents.pop();
        this.state = this.states.pop();
    }

    // Flow sequence handlers.

    private void expectFlowSequence() throws IOException {
        this.writeIndicator("[", true, true, false);
        this.flowLevel++;
        this.increaseIndent(true, false);
        if (this.prettyFlow) {
            this.writeIndent();
        }
        this.state = new ExpectFirstFlowSequenceItem();
    }

    // Flow mapping handlers.

    private void expectFlowMapping() throws IOException {
        this.writeIndicator("{", true, true, false);
        this.flowLevel++;
        this.increaseIndent(true, false);
        if (this.prettyFlow) {
            this.writeIndent();
        }
        this.state = new ExpectFirstFlowMappingKey();
    }

    // Block sequence handlers.

    private void expectBlockSequence() throws IOException {
        boolean indentLess = this.mappingContext && !this.indention;
        this.increaseIndent(false, indentLess);
        this.state = new ExpectFirstBlockSequenceItem();
    }

    // Block mapping handlers.
    private void expectBlockMapping() throws IOException {
        this.increaseIndent(false, false);
        this.state = new ExpectFirstBlockMappingKey();
    }

    // Checkers.

    private boolean checkEmptySequence() {
        return (this.event instanceof SequenceStartEvent) && !this.events.isEmpty()
                && (this.events.peek() instanceof SequenceEndEvent);
    }

    private boolean checkEmptyMapping() {
        return (this.event instanceof MappingStartEvent) && !this.events.isEmpty()
                && (this.events.peek() instanceof MappingEndEvent);
    }

    boolean checkEmptyDocument() {
        if (!(this.event instanceof DocumentStartEvent) || this.events.isEmpty()) {
            return false;
        }
        Event event = this.events.peek();
        if (event instanceof ScalarEvent) {
            ScalarEvent e = (ScalarEvent) event;
            return (e.getAnchor() == null) && (e.getTag() == null) && (e.getImplicit() != null)
                    && e.getValue().isEmpty();
        }
        return false;
    }

    boolean checkSimpleKey() {
        int length = 0;
        if ((this.event instanceof NodeEvent) && (((NodeEvent) this.event).getAnchor() != null)) {
            if (this.preparedAnchor == null) {
                this.preparedAnchor = prepareAnchor(((NodeEvent) this.event).getAnchor());
            }
            length += this.preparedAnchor.length();
        }
        String tag = null;
        if (this.event instanceof ScalarEvent) {
            tag = ((ScalarEvent) this.event).getTag();
        } else if (this.event instanceof CollectionStartEvent) {
            tag = ((CollectionStartEvent) this.event).getTag();
        }
        if (tag != null) {
            if (this.preparedTag == null) {
                this.preparedTag = this.prepareTag(tag);
            }
            length += this.preparedTag.length();
        }
        if (this.event instanceof ScalarEvent) {
            if (this.analysis == null) {
                this.analysis = this.analyzeScalar(((ScalarEvent) this.event).getValue());
            }
            length += this.analysis.scalar.length();
        }
        return (length < SMALL_LENGTH)
                && ((this.event instanceof AliasEvent)
                        || ((this.event instanceof ScalarEvent) && !((this.analysis == null) || this.analysis.empty)
                                && !this.analysis.multiline)
                        || this.checkEmptySequence() || this.checkEmptyMapping());
    }

    // Anchor, Tag, and Scalar processors.

    private void processAnchor(String indicator) throws IOException {
        assert this.event != null;
        NodeEvent ev = (NodeEvent) this.event;
        if (ev.getAnchor() == null) {
            this.preparedAnchor = null;
            return;
        }
        if (this.preparedAnchor == null) {
            this.preparedAnchor = prepareAnchor(ev.getAnchor());
        }
        this.writeIndicator(indicator + this.preparedAnchor, true, false, false);
        this.preparedAnchor = null;
    }

    private void processTag() throws IOException {
        assert this.event != null;
        String tag;
        if (this.event instanceof ScalarEvent) {
            ScalarEvent ev = (ScalarEvent) this.event;
            tag = ev.getTag();
            if (this.style == null) {
                this.style = this.chooseScalarStyle();
            }
            if ((!this.canonical || (tag == null))
                    && (((this.style == null) && ev.getImplicit().canOmitTagInPlainScalar())
                            || ((this.style != null) && ev.getImplicit().canOmitTagInNonPlainScalar()))) {
                this.preparedTag = null;
                return;
            }
            if (ev.getImplicit().canOmitTagInPlainScalar() && (tag == null)) {
                tag = "!";
                this.preparedTag = null;
            }
        } else {
            CollectionStartEvent ev = (CollectionStartEvent) this.event;
            tag = ev.getTag();
            if ((!this.canonical || (tag == null)) && ev.getImplicit()) {
                this.preparedTag = null;
                return;
            }
        }
        if (tag == null) {
            throw new EmitterException("tag is not specified");
        }
        if (this.preparedTag == null) {
            this.preparedTag = this.prepareTag(tag);
        }
        this.writeIndicator(this.preparedTag, true, false, false);
        this.preparedTag = null;
    }

    @Nullable
    private Character chooseScalarStyle() {
        assert this.event != null;
        ScalarEvent ev = (ScalarEvent) this.event;
        if (this.analysis == null) {
            this.analysis = this.analyzeScalar(ev.getValue());
        }
        if (((ev.getStyle() != null) && (ev.getStyle() == '"')) || this.canonical) {
            return '"';
        }
        if ((ev.getStyle() == null) && ev.getImplicit().canOmitTagInPlainScalar()) {
            if (!(this.simpleKeyContext && (this.analysis.empty || this.analysis.multiline))
                    && (((this.flowLevel != 0) && this.analysis.allowFlowPlain)
                            || ((this.flowLevel == 0) && this.analysis.allowBlockPlain))) {
                return null;
            }
        }
        if ((ev.getStyle() != null) && ((ev.getStyle() == '|') || (ev.getStyle() == '>'))) {
            if ((this.flowLevel == 0) && !this.simpleKeyContext && this.analysis.allowBlock) {
                return ev.getStyle();
            }
        }
        if ((ev.getStyle() == null) || (ev.getStyle() == '\'')) {
            if (this.analysis.allowSingleQuoted && !(this.simpleKeyContext && this.analysis.multiline)) {
                return '\'';
            }
        }
        return '"';
    }

    private void processScalar() throws IOException {
        assert this.event != null;
        ScalarEvent ev = (ScalarEvent) this.event;
        if (this.analysis == null) {
            this.analysis = this.analyzeScalar(ev.getValue());
        }
        if (this.style == null) {
            this.style = this.chooseScalarStyle();
        }
        boolean split = !this.simpleKeyContext && this.splitLines;
        if (this.style == null) {
            this.writePlain(this.analysis.scalar, split);
        } else {
            switch (this.style) {
            case '"':
                this.writeDoubleQuoted(this.analysis.scalar, split);
                break;
            case '\'':
                this.writeSingleQuoted(this.analysis.scalar, split);
                break;
            case '>':
                this.writeFolded(this.analysis.scalar, split);
                break;
            case '|':
                this.writeLiteral(this.analysis.scalar);
                break;
            default:
                throw new YAMLException("Unexpected style: " + this.style);
            }
        }
        this.analysis = null;
        this.style = null;
    }

    // Analyzers.

    String prepareVersion(Version version) {
        if (version.major() != 1) {
            throw new EmitterException("unsupported YAML version: " + version);
        }
        return version.getRepresentation();
    }

    private static final Pattern HANDLE_FORMAT = Pattern.compile("^![-_\\w]*!$");

    String prepareTagHandle(String handle) {
        if (handle.isEmpty()) {
            throw new EmitterException("tag handle must not be empty");
        }
        if ((handle.charAt(0) != '!') || (handle.charAt(handle.length() - 1) != '!')) {
            throw new EmitterException("tag handle must start and end with '!': " + handle);
        }
        if (!"!".equals(handle) && !HANDLE_FORMAT.matcher(handle).matches()) {
            throw new EmitterException("invalid character in the tag handle: " + handle);
        }
        return handle;
    }

    String prepareTagPrefix(String prefix) {
        if (prefix.isEmpty()) {
            throw new EmitterException("tag prefix must not be empty");
        }
        StringBuilder chunks = new StringBuilder();
        int start = 0;
        int end = 0;
        if (prefix.charAt(0) == '!') {
            end = 1;
        }
        while (end < prefix.length()) {
            end++;
        }
        if (start < end) {
            chunks.append(prefix.substring(start, end));
        }
        return chunks.toString();
    }

    private String prepareTag(String tag) {
        if (tag.isEmpty()) {
            throw new EmitterException("tag must not be empty");
        }
        if ("!".equals(tag)) {
            return tag;
        }
        String handle = null;
        String suffix = tag;
        // shall the tag prefixes be sorted as in PyYAML?
        for (String prefix : this.tagPrefixes.keySet()) {
            if (tag.startsWith(prefix) && ("!".equals(prefix) || (prefix.length() < tag.length()))) {
                handle = prefix;
            }
        }
        if (handle != null) {
            suffix = tag.substring(handle.length());
            handle = this.tagPrefixes.get(handle);
        }

        int end = suffix.length();
        String suffixText = (end > 0) ? suffix.substring(0, end) : "";

        if (handle != null) {
            return handle + suffixText;
        }
        return "!<" + suffixText + ">";
    }

    private static final Pattern ANCHOR_FORMAT = Pattern.compile("^[-_\\w]*$");

    static String prepareAnchor(String anchor) {
        if (anchor.isEmpty()) {
            throw new EmitterException("anchor must not be empty");
        }
        if (!ANCHOR_FORMAT.matcher(anchor).matches()) {
            throw new EmitterException("invalid character in the anchor: " + anchor);
        }
        return anchor;
    }

    private ScalarAnalysis analyzeScalar(String scalar) {
        // Empty scalar is a special case.
        if (scalar.isEmpty()) {
            return new ScalarAnalysis(scalar, true, false, false, true, true, false);
        }
        // Indicators and special characters.
        boolean blockIndicators = false;
        boolean flowIndicators = false;
        boolean lineBreaks = false;
        boolean specialCharacters = false;

        // Important whitespace combinations.
        boolean leadingSpace = false;
        boolean leadingBreak = false;
        boolean trailingSpace = false;
        boolean trailingBreak = false;
        boolean breakSpace = false;
        boolean spaceBreak = false;

        // Check document indicators.
        if (scalar.startsWith("---") || scalar.startsWith("...")) {
            blockIndicators = true;
            flowIndicators = true;
        }
        // First character or preceded by a whitespace.
        boolean precededByWhitespace = true;
        boolean followedByWhitespace = (scalar.length() == 1) || Constant.NULL_BL_T_LINEBR.has(scalar.charAt(1));
        // The previous character is a space.
        boolean previousSpace = false;

        // The previous character is a break.
        boolean previousBreak = false;

        int index = 0;

        while (index < scalar.length()) {
            char ch = scalar.charAt(index);
            // Check for indicators.
            if (index == 0) {
                // Leading indicators are special characters.
                if ("#,[]{}&*!|>\'\"%@`".indexOf(ch) != -1) {
                    flowIndicators = true;
                    blockIndicators = true;
                }
                if ((ch == '?') || (ch == ':')) {
                    flowIndicators = true;
                    if (followedByWhitespace) {
                        blockIndicators = true;
                    }
                }
                if ((ch == '-') && followedByWhitespace) {
                    flowIndicators = true;
                    blockIndicators = true;
                }
            } else {
                // Some indicators cannot appear within a scalar as well.
                if (",?[]{}".indexOf(ch) != -1) {
                    flowIndicators = true;
                }
                if (ch == ':') {
                    flowIndicators = true;
                    if (followedByWhitespace) {
                        blockIndicators = true;
                    }
                }
                if ((ch == '#') && precededByWhitespace) {
                    flowIndicators = true;
                    blockIndicators = true;
                }
            }
            // Check for line breaks, special, and unicode characters.
            boolean isLineBreak = Constant.LINEBR.has(ch);
            if (isLineBreak) {
                lineBreaks = true;
            }
            if (!((ch == '\n') || (('\u0020' <= ch) && (ch <= '\u007E')))) {
                if (((ch == '\u0085') || (('\u00A0' <= ch) && (ch <= '\uD7FF'))
                        || (('\uE000' <= ch) && (ch <= '\uFFFD'))) && (ch != '\uFEFF')) {
                    // unicode is used
                    if (!this.allowUnicode) {
                        specialCharacters = true;
                    }
                } else {
                    specialCharacters = true;
                }
            }
            // Detect important whitespace combinations.
            if (ch == ' ') {
                if (index == 0) {
                    leadingSpace = true;
                }
                if (index == (scalar.length() - 1)) {
                    trailingSpace = true;
                }
                if (previousBreak) {
                    breakSpace = true;
                }
                previousSpace = true;
                previousBreak = false;
            } else if (isLineBreak) {
                if (index == 0) {
                    leadingBreak = true;
                }
                if (index == (scalar.length() - 1)) {
                    trailingBreak = true;
                }
                if (previousSpace) {
                    spaceBreak = true;
                }
                previousSpace = false;
                previousBreak = true;
            } else {
                previousSpace = false;
                previousBreak = false;
            }

            // Prepare for the next character.
            index++;
            precededByWhitespace = Constant.NULL_BL_T.has(ch) || isLineBreak;
            followedByWhitespace = ((index + 1) >= scalar.length())
                    || (Constant.NULL_BL_T.has(scalar.charAt(index + 1))) || isLineBreak;
        }
        // Let's decide what styles are allowed.
        boolean allowFlowPlain = true;
        boolean allowBlockPlain = true;
        boolean allowSingleQuoted = true;
        boolean allowBlock = true;
        // Leading and trailing whitespaces are bad for plain scalars.
        if (leadingSpace || leadingBreak || trailingSpace || trailingBreak) {
            allowFlowPlain = allowBlockPlain = false;
        }
        // We do not permit trailing spaces for block scalars.
        if (trailingSpace) {
            allowBlock = false;
        }
        // Spaces at the beginning of a new line are only acceptable for block
        // scalars.
        if (breakSpace) {
            allowFlowPlain = allowBlockPlain = allowSingleQuoted = false;
        }
        // Spaces followed by breaks, as well as special character are only
        // allowed for double quoted scalars.
        if (spaceBreak || specialCharacters) {
            allowFlowPlain = allowBlockPlain = allowSingleQuoted = allowBlock = false;
        }
        // Although the plain scalar writer supports breaks, we never emit
        // multiline plain scalars in the flow context.
        if (lineBreaks) {
            allowFlowPlain = false;
        }
        // Flow indicators are forbidden for flow plain scalars.
        if (flowIndicators) {
            allowFlowPlain = false;
        }
        // Block indicators are forbidden for block plain scalars.
        if (blockIndicators) {
            allowBlockPlain = false;
        }

        return new ScalarAnalysis(scalar, false, lineBreaks, allowFlowPlain, allowBlockPlain, allowSingleQuoted,
                allowBlock);
    }

    // Writers.

    void flushStream() throws IOException {
        this.stream.flush();
    }

    void writeStreamStart() {
        // BOM is written by Writer.
    }

    void writeStreamEnd() throws IOException {
        this.flushStream();
    }

    void writeIndicator(String indicator, boolean needWhitespace, boolean whitespace, boolean indentation)
            throws IOException {
        if (!this.whitespace && needWhitespace) {
            this.column++;
            this.stream.write(SPACE);
        }
        this.whitespace = whitespace;
        this.indention = this.indention && indentation;
        this.column += indicator.length();
        this.openEnded = false;
        this.stream.write(indicator);
    }

    void writeIndentMod(int mod) throws IOException {
        int indent;
        if (this.indent != null) {
            indent = this.indent;
        } else {
            indent = 0;
        }
        if ((indent > 0) && ((indent + mod) < 0)) {
            this.writeIndent(0);
        }
        this.writeIndent(indent + mod);
    }

    void writeIndent() throws IOException {
        this.writeIndentMod(0);
    }

    void writeIndentForce(@Nullable Integer realIndent) throws IOException {
        int indent;
        if (realIndent != null) {
            indent = realIndent;
        } else {
            indent = 0;
        }
        this.writeWhitespace(indent - this.column);
    }

    void writeIndent(int indent) throws IOException {
        if (!this.indention || (this.column > indent) || ((this.column == indent) && !this.whitespace)) {
            this.writeLineBreak(null);
        }

        this.writeWhitespace(indent - this.column);
    }

    void writeWhitespace(int length) throws IOException {
        if (length <= 0) {
            return;
        }
        this.whitespace = true;
        char[] data = new char[length];
        for (int i = 0; i < data.length; i++) {
            data[i] = ' ';
        }
        this.column += length;
        this.stream.write(data);
    }

    private void writeLineBreak(@Nullable String data) throws IOException {
        this.whitespace = true;
        this.indention = true;
        this.column = 0;
        if (data == null) {
            this.stream.write(this.bestLineBreak);
        } else {
            this.stream.write(data);
        }
    }

    void writeVersionDirective(String versionText) throws IOException {
        this.stream.write("%YAML ");
        this.stream.write(versionText);
        this.writeLineBreak(null);
    }

    void writeTagDirective(String handleText, String prefixText) throws IOException {
        // XXX: not sure 4 invocations better then StringBuilders created by str
        // + str
        this.stream.write("%TAG ");
        this.stream.write(handleText);
        this.stream.write(SPACE);
        this.stream.write(prefixText);
        this.writeLineBreak(null);
    }

    // Scalar streams.
    private void writeSingleQuoted(String text, boolean split) throws IOException {
        this.writeIndicator("'", true, false, false);
        boolean spaces = false;
        boolean breaks = false;
        int start = 0, end = 0;
        char ch;
        while (end <= text.length()) {
            ch = 0;
            if (end < text.length()) {
                ch = text.charAt(end);
            }
            if (spaces) {
                if ((ch == 0) || (ch != ' ')) {
                    if (((start + 1) == end) && (this.column > this.bestWidth) && split && (start != 0)
                            && (end != text.length())) {
                        this.writeIndent();
                    } else {
                        int len = end - start;
                        this.column += len;
                        this.stream.write(text, start, len);
                    }
                    start = end;
                }
            } else if (breaks) {
                if ((ch == 0) || Constant.LINEBR.hasNo(ch)) {
                    if (text.charAt(start) == '\n') {
                        this.writeLineBreak(null);
                    }
                    String data = text.substring(start, end);
                    for (char br : data.toCharArray()) {
                        if (br == '\n') {
                            this.writeLineBreak(null);
                        } else {
                            this.writeLineBreak(String.valueOf(br));
                        }
                    }
                    this.writeIndent();
                    start = end;
                }
            } else {
                if (Constant.LINEBR.has(ch, "\0 \'")) {
                    if (start < end) {
                        int len = end - start;
                        this.column += len;
                        this.stream.write(text, start, len);
                        start = end;
                    }
                }
            }
            if (ch == '\'') {
                this.column += 2;
                this.stream.write("''");
                start = end + 1;
            }
            if (ch != 0) {
                spaces = ch == ' ';
                breaks = Constant.LINEBR.has(ch);
            }
            end++;
        }
        this.writeIndicator("'", false, false, false);
    }

    private void writeDoubleQuoted(String text, boolean split) throws IOException {
        this.writeIndicator("\"", true, false, false);
        int start = 0;
        int end = 0;
        while (end <= text.length()) {
            Character ch = null;
            if (end < text.length()) {
                ch = text.charAt(end);
            }
            if ((ch == null) || ("\"\\\u0085\u2028\u2029\uFEFF".indexOf(ch) != -1)
                    || !(('\u0020' <= ch) && (ch <= '\u007E'))) {
                if (start < end) {
                    int len = end - start;
                    this.column += len;
                    this.stream.write(text, start, len);
                    start = end;
                }
                if (ch != null) {
                    String data;
                    if (ESCAPE_REPLACEMENTS.containsKey(ch)) {
                        data = "\\" + ESCAPE_REPLACEMENTS.get(ch);
                    } else if (!this.allowUnicode || !StreamReader.isPrintable(ch)) {
                        // if !allowUnicode or the character is not printable,
                        // we must encode it
                        if (ch <= '\u00FF') {
                            String s = "0" + Integer.toString(ch, HEX_RADIX);
                            data = "\\x" + s.substring(s.length() - 2);
                        } else if ((ch >= '\uD800') && (ch <= '\uDBFF')) {
                            if ((end + 1) < text.length()) {
                                Character ch2 = text.charAt(++end);
                                String s = "000" + Long.toHexString(Character.toCodePoint(ch, ch2));
                                data = "\\U" + s.substring(s.length() - 8);
                            } else {
                                String s = "000" + Integer.toString(ch, HEX_RADIX);
                                data = "\\u" + s.substring(s.length() - 4);
                            }
                        } else {
                            String s = "000" + Integer.toString(ch, HEX_RADIX);
                            data = "\\u" + s.substring(s.length() - 4);
                        }
                    } else {
                        data = String.valueOf(ch);
                    }
                    this.column += data.length();
                    this.stream.write(data);
                    start = end + 1;
                }
            }
            if (((0 < end) && (end < (text.length() - 1))) && ((Objects.equals(ch, ' ')) || (start >= end))
                    && ((this.column + (end - start)) > this.bestWidth) && split) {
                String data;
                if (start >= end) {
                    data = "\\";
                } else {
                    data = text.substring(start, end) + "\\";
                }
                if (start < end) {
                    start = end;
                }
                this.column += data.length();
                this.stream.write(data);
                this.writeIndent();
                this.whitespace = false;
                this.indention = false;
                if (text.charAt(start) == ' ') {
                    data = "\\";
                    this.column += data.length();
                    this.stream.write(data);
                }
            }
            end += 1;
        }
        this.writeIndicator("\"", false, false, false);
    }

    private String determineBlockHints(String text) {
        StringBuilder hints = new StringBuilder();
        //        if (Constant.LINEBR.has(text.charAt(0), " ")) disabled, always add indent marker to prevent formatting problems.
        {
            hints.append(this.bestIndent);
        }
        char ch1 = text.charAt(text.length() - 1);
        if (Constant.LINEBR.hasNo(ch1)) {
            hints.append("-");
        } else if ((text.length() == 1) || Constant.LINEBR.has(text.charAt(text.length() - 2))) {
            hints.append("+");
        }
        return hints.toString();
    }

    private String constructBorder(String longCommentBorder, int size) {
        char[] chars = longCommentBorder.toCharArray();
        StringBuilder sb = new StringBuilder(size);
        while (true) {
            for (char aChar : chars) {
                sb.append(aChar);
                if (size-- == 0) {
                    return sb.toString();
                }
            }
        }
    }

    private String createRightBorder(String comment, int longest, String commentRightBorder) {
        int commentLength = comment.length() + 2;
        String rightBorder;
        if (commentLength < longest) {
            rightBorder = StringUtils.repeat(' ',
                    DioriteMathUtils.ceil(((double) (longest - commentLength)) / commentRightBorder.length()))
                    + '#';
        } else {
            rightBorder = commentRightBorder;
        }
        return "# " + comment + rightBorder;
    }

    private int getLongestCommentLength(String[] comments) {
        int longest = -1;
        for (String comment : comments) {
            int commentLength = comment.length();
            if (commentLength > longest) {
                longest = commentLength;
            }
        }
        return longest;
    }

    private void printComments(String... comments) throws IOException {
        int length = comments.length;
        boolean longCommentBorderStartsWithComment = this.longCommentBorderStartsWithComment;
        String commentRightBorder = this.longCommentRightBorder;
        String border = null;
        int longest = -1;
        if (length >= this.longCommentThreshold) {
            longest = this.getLongestCommentLength(comments) + (longCommentBorderStartsWithComment ? 3 : 4);
            border = this.constructBorder(this.longCommentBorder, longest);
            this.writeIndent();
            if (!longCommentBorderStartsWithComment) {
                this.stream.write('#');
            }
            this.stream.write(border);
        }
        boolean hasRightBorder = (border != null) && !commentRightBorder.isEmpty();
        for (int i = 0; i < comments.length; i++) {
            String comment = comments[i];
            if (i != 0) {
                this.writeLineBreak(null);
            }
            this.writeIndentMod(0);
            if (hasRightBorder) {
                this.stream.write(this.createRightBorder(comment, longest, commentRightBorder));
            } else {
                this.stream.write("# " + comment);
            }
        }
        if (border != null) {
            this.writeLineBreak(null);
            this.writeIndent();
            if (!longCommentBorderStartsWithComment) {
                this.stream.write('#');
            }
            this.stream.write(border);
        }
    }

    //    void writeComment(int firstNewLine, int lastNewLine, String... comment) throws IOException
    //    {
    //        while (firstNewLine-- > 0)
    //        {
    //            this.writeLineBreak(null);
    //        }
    //        List<String> comments = new ArrayList<>(comment.length * 2);
    //        for (String s : comment)
    //        {
    //            Collections.addAll(comments, StringUtils.splitPreserveAllTokens(s, '\n'));
    //        }
    //        this.printComments(comments.toArray(new String[comments.size()]));
    //        while (lastNewLine-- > 0)
    //        {
    //            this.writeLineBreak(null);
    //        }
    //    }

    void writeCommentSafe(@Nullable String comment, int firstNewLine, int lastNewLine) throws IOException {
        if ((comment == null) || comment.isEmpty()) {
            return;
        }
        this.writeComment(comment, firstNewLine, lastNewLine);
    }

    void writeComment(String comment, int firstNewLine, int lastNewLine) throws IOException {
        while (firstNewLine-- > 0) {
            this.writeLineBreak(null);
        }
        this.printComments(StringUtils.splitPreserveAllTokens(comment, '\n'));
        while (lastNewLine-- > 0) {
            this.writeLineBreak(null);
        }
    }

    void writeFolded(String text, boolean split) throws IOException {
        String hints = this.determineBlockHints(text);
        this.writeIndicator(">" + hints, true, false, false);
        if (!hints.isEmpty() && (hints.charAt(hints.length() - 1) == '+')) {
            this.openEnded = true;
        }
        this.writeLineBreak(null);
        boolean leadingSpace = true;
        boolean spaces = false;
        boolean breaks = true;
        int start = 0, end = 0;
        while (end <= text.length()) {
            char ch = 0;
            if (end < text.length()) {
                ch = text.charAt(end);
            }
            if (breaks) {
                if ((ch == 0) || Constant.LINEBR.hasNo(ch)) {
                    if (!leadingSpace && (ch != 0) && (ch != ' ') && (text.charAt(start) == '\n')) {
                        this.writeLineBreak(null);
                    }
                    leadingSpace = ch == ' ';
                    String data = text.substring(start, end);
                    for (char br : data.toCharArray()) {
                        if (br == '\n') {
                            this.writeLineBreak(null);
                        } else {
                            this.writeLineBreak(String.valueOf(br));
                        }
                    }
                    if (ch != 0) {
                        this.writeIndent();
                    }
                    start = end;
                }
            } else if (spaces) {
                if (ch != ' ') {
                    if (((start + 1) == end) && (this.column > this.bestWidth) && split) {
                        this.writeIndent();
                    } else {
                        int len = end - start;
                        this.column += len;
                        this.stream.write(text, start, len);
                    }
                    start = end;
                }
            } else {
                if (Constant.LINEBR.has(ch, "\0 ")) {
                    int len = end - start;
                    this.column += len;
                    this.stream.write(text, start, len);
                    if (ch == 0) {
                        this.writeLineBreak(null);
                    }
                    start = end;
                }
            }
            if (ch != 0) {
                breaks = Constant.LINEBR.has(ch);
                spaces = ch == ' ';
            }
            end++;
        }
    }

    void writeLiteral(String text) throws IOException {
        String hints = this.determineBlockHints(text);
        this.writeIndicator("|" + hints, true, false, false);
        if (!hints.isEmpty() && ((hints.charAt(hints.length() - 1)) == '+')) {
            this.openEnded = true;
        }
        this.writeLineBreak(null);
        boolean breaks = true;
        int start = 0, end = 0;
        while (end <= text.length()) {
            char ch = 0;
            if (end < text.length()) {
                ch = text.charAt(end);
            }
            if (breaks) {
                if ((ch == 0) || Constant.LINEBR.hasNo(ch)) {
                    String data = text.substring(start, end);
                    for (char br : data.toCharArray()) {
                        if (br == '\n') {
                            this.writeLineBreak(null);
                        } else {
                            this.writeLineBreak(String.valueOf(br));
                        }
                    }
                    if (ch != 0) {
                        this.writeIndent();
                    }
                    start = end;
                }
            } else {
                if ((ch == 0) || Constant.LINEBR.has(ch)) {
                    this.stream.write(text, start, end - start);
                    if (ch == 0) {
                        this.writeLineBreak(null);
                    }
                    start = end;
                }
            }
            if (ch != 0) {
                breaks = Constant.LINEBR.has(ch);
            }
            end++;
        }
    }

    void writePlain(String text, boolean split) throws IOException {
        if (this.rootContext) {
            this.openEnded = true;
        }
        if (text.isEmpty()) {
            return;
        }
        if (!this.whitespace) {
            this.column++;
            this.stream.write(SPACE);
        }
        this.whitespace = false;
        this.indention = false;
        boolean spaces = false;
        boolean breaks = false;
        int start = 0, end = 0;
        while (end <= text.length()) {
            char ch = 0;
            if (end < text.length()) {
                ch = text.charAt(end);
            }
            if (spaces) {
                if (ch != ' ') {
                    if (((start + 1) == end) && (this.column > this.bestWidth) && split) {
                        this.writeIndent();
                        this.whitespace = false;
                        this.indention = false;
                    } else {
                        int len = end - start;
                        this.column += len;
                        this.stream.write(text, start, len);
                    }
                    start = end;
                }
            } else if (breaks) {
                if (Constant.LINEBR.hasNo(ch)) {
                    if (text.charAt(start) == '\n') {
                        this.writeLineBreak(null);
                    }
                    String data = text.substring(start, end);
                    for (char br : data.toCharArray()) {
                        if (br == '\n') {
                            this.writeLineBreak(null);
                        } else {
                            this.writeLineBreak(String.valueOf(br));
                        }
                    }
                    this.writeIndent();
                    this.whitespace = false;
                    this.indention = false;
                    start = end;
                }
            } else {
                if ((ch == 0) || Constant.LINEBR.has(ch)) {
                    int len = end - start;
                    this.column += len;
                    this.stream.write(text, start, len);
                    start = end;
                }
            }
            if (ch != 0) {
                spaces = ch == ' ';
                breaks = Constant.LINEBR.has(ch);
            }
            end++;
        }
    }
}