From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Jake Potrebic Date: Sat, 1 Jan 2022 21:24:50 -0800 Subject: [PATCH] Fix saving configs with more long comments This is a bug with snakeyaml PR: https://bitbucket.org/snakeyaml/snakeyaml/pull-requests/3 Issue: https://bitbucket.org/snakeyaml/snakeyaml/issues/518/comments-could-cause-queue-full Added the entire Emitter class from snakeyaml because dev-imports doesn't work with non-mojang-added dependencies Replacement for upstream: https://hub.spigotmc.org/stash/projects/SPIGOT/repos/bukkit/commits/a7505b3cd0498baca152777767f0e4ddebbe4d1a diff --git a/src/main/java/org/yaml/snakeyaml/emitter/Emitter.java b/src/main/java/org/yaml/snakeyaml/emitter/Emitter.java new file mode 100644 index 0000000000000000000000000000000000000000..ca32c00775f3d98abea39bbfeb0268bb9efbc12b --- /dev/null +++ b/src/main/java/org/yaml/snakeyaml/emitter/Emitter.java @@ -0,0 +1,1682 @@ +/** + * Copyright (c) 2008, SnakeYAML + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.yaml.snakeyaml.emitter; + +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.DumperOptions.ScalarStyle; +import org.yaml.snakeyaml.DumperOptions.Version; +import org.yaml.snakeyaml.comments.CommentEventsCollector; +import org.yaml.snakeyaml.comments.CommentLine; +import org.yaml.snakeyaml.comments.CommentType; +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.CommentEvent; +import org.yaml.snakeyaml.events.DocumentEndEvent; +import org.yaml.snakeyaml.events.DocumentStartEvent; +import org.yaml.snakeyaml.events.Event; +import org.yaml.snakeyaml.events.Event.ID; +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.events.StreamStartEvent; +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 java.io.IOException; +import java.io.Writer; +import java.util.ArrayDeque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + *
+ * 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
+ * 
+ */ +public final class Emitter implements Emitable { + public static final int MIN_INDENT = 1; + public static final int MAX_INDENT = 10; + private static final char[] SPACE = {' '}; + + private static final Pattern SPACES_PATTERN = Pattern.compile("\\s"); + private static final Set INVALID_ANCHOR = new HashSet(); + static { + INVALID_ANCHOR.add('['); + INVALID_ANCHOR.add(']'); + INVALID_ANCHOR.add('{'); + INVALID_ANCHOR.add('}'); + INVALID_ANCHOR.add(','); + INVALID_ANCHOR.add('*'); + INVALID_ANCHOR.add('&'); + } + + private static final Map ESCAPE_REPLACEMENTS = new HashMap(); + 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"); + } + + private final static Map DEFAULT_TAG_PREFIXES = new LinkedHashMap(); + static { + DEFAULT_TAG_PREFIXES.put("!", "!"); + DEFAULT_TAG_PREFIXES.put(Tag.PREFIX, "!!"); + } + // The stream should have the methods `write` and possibly `flush`. + private final Writer stream; + + // Encoding is defined by Writer (cannot be overridden by STREAM-START.) + // private Charset encoding; + + // Emitter is a state machine with a stack of states to handle nested + // structures. + private final ArrayStack states; + private EmitterState state; + + // Current event and the event queue. + private final Queue events; + private Event event; + + // The current indentation level and the stack of previous indents. + private final ArrayStack indents; + private Integer indent; + + // Flow level. + private 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 + private int column; + private boolean whitespace; + private boolean indention; + private boolean openEnded; + + // Formatting details. + private final Boolean canonical; + // pretty print flow by adding extra line breaks + private final Boolean prettyFlow; + + private final boolean allowUnicode; + private int bestIndent; + private final int indicatorIndent; + private final boolean indentWithIndicator; + private int bestWidth; + private final char[] bestLineBreak; + private final boolean splitLines; + private final int maxSimpleKeyLength; + private final boolean emitComments; + + // Tag prefixes. + private Map tagPrefixes; + + // Prepared anchor and tag. + private String preparedAnchor; + private String preparedTag; + + // Scalar analysis and style. + private ScalarAnalysis analysis; + private DumperOptions.ScalarStyle style; + + // Comment processing + private final CommentEventsCollector blockCommentsCollector; + private final CommentEventsCollector inlineCommentsCollector; + + + public Emitter(Writer stream, DumperOptions opts) { + // 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 ArrayDeque<>(100); // Paper - allow more than 100 events (or comments) + // 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. + mappingContext = false; + simpleKeyContext = false; + + // + // Characteristics of the last emitted character: + // - current position. + // - is it a whitespace? + // - is it an indention character + // (indentation space, '-', '?', or ':')? + column = 0; + whitespace = true; + indention = true; + + // Whether the document requires an explicit document indicator + 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.indentWithIndicator = opts.getIndentWithIndicator(); + this.bestWidth = 80; + if (opts.getWidth() > this.bestIndent * 2) { + this.bestWidth = opts.getWidth(); + } + this.bestLineBreak = opts.getLineBreak().getString().toCharArray(); + this.splitLines = opts.getSplitLines(); + this.maxSimpleKeyLength = opts.getMaxSimpleKeyLength(); + this.emitComments = opts.isProcessComments(); + + // Tag prefixes. + this.tagPrefixes = new LinkedHashMap(); + + // Prepared anchor and tag. + this.preparedAnchor = null; + this.preparedTag = null; + + // Scalar analysis and style. + this.analysis = null; + this.style = null; + + // Comment processing + this.blockCommentsCollector = new CommentEventsCollector(events, + CommentType.BLANK_LINE, CommentType.BLOCK); + this.inlineCommentsCollector = new CommentEventsCollector(events, + CommentType.IN_LINE); + } + + public void emit(Event event) throws IOException { + this.events.add(event); + while (!needMoreEvents()) { + this.event = this.events.poll(); + this.state.expect(); + this.event = null; + } + } + + // In some cases, we wait for a few next events before emitting. + + private boolean needMoreEvents() { + if (events.isEmpty()) { + return true; + } + + Iterator iter = events.iterator(); + Event event = iter.next(); // FIXME why without check ??? + while(event instanceof CommentEvent) { + if (!iter.hasNext()) { + return true; + } + event = iter.next(); + } + + if (event instanceof DocumentStartEvent) { + return needEvents(iter, 1); + } else if (event instanceof SequenceStartEvent) { + return needEvents(iter, 2); + } else if (event instanceof MappingStartEvent) { + return needEvents(iter, 3); + } else if (event instanceof StreamStartEvent) { + return needEvents(iter, 2); + } else if (event instanceof StreamEndEvent) { + return false; + } else if (emitComments) { + return needEvents(iter, 1); + } + return false; + } + + private boolean needEvents(Iterator iter, int count) { + int level = 0; + int actualCount = 0; + while (iter.hasNext()) { + Event event = iter.next(); + if (event instanceof CommentEvent) { + continue; + } + actualCount++; + 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 actualCount < count; + } + + private void increaseIndent(boolean flow, boolean indentless) { + indents.push(indent); + if (indent == null) { + if (flow) { + indent = bestIndent; + } else { + indent = 0; + } + } else if (!indentless) { + this.indent += bestIndent; + } + } + + // States + + // Stream handlers. + + private class ExpectStreamStart implements EmitterState { + public void expect() throws IOException { + if (event instanceof StreamStartEvent) { + writeStreamStart(); + state = new ExpectFirstDocumentStart(); + } else { + throw new EmitterException("expected StreamStartEvent, but got " + event); + } + } + } + + private class ExpectNothing implements EmitterState { + public void expect() throws IOException { + throw new EmitterException("expecting nothing, but got " + event); + } + } + + // Document handlers. + + private class ExpectFirstDocumentStart implements EmitterState { + public void expect() throws IOException { + new ExpectDocumentStart(true).expect(); + } + } + + private class ExpectDocumentStart implements EmitterState { + private final boolean first; + + public ExpectDocumentStart(boolean first) { + this.first = first; + } + + public void expect() throws IOException { + if (event instanceof DocumentStartEvent) { + DocumentStartEvent ev = (DocumentStartEvent) event; + if ((ev.getVersion() != null || ev.getTags() != null) && openEnded) { + writeIndicator("...", true, false, false); + writeIndent(); + } + if (ev.getVersion() != null) { + String versionText = prepareVersion(ev.getVersion()); + writeVersionDirective(versionText); + } + tagPrefixes = new LinkedHashMap(DEFAULT_TAG_PREFIXES); + if (ev.getTags() != null) { + Set handles = new TreeSet(ev.getTags().keySet()); + for (String handle : handles) { + String prefix = ev.getTags().get(handle); + tagPrefixes.put(prefix, handle); + String handleText = prepareTagHandle(handle); + String prefixText = prepareTagPrefix(prefix); + writeTagDirective(handleText, prefixText); + } + } + boolean implicit = first && !ev.getExplicit() && !canonical + && ev.getVersion() == null + && (ev.getTags() == null || ev.getTags().isEmpty()) + && !checkEmptyDocument(); + if (!implicit) { + writeIndent(); + writeIndicator("---", true, false, false); + if (canonical) { + writeIndent(); + } + } + state = new ExpectDocumentRoot(); + } else if (event instanceof StreamEndEvent) { + writeStreamEnd(); + state = new ExpectNothing(); + } else if (event instanceof CommentEvent) { + blockCommentsCollector.collectEvents(event); + writeBlockComment(); + // state = state; remains unchanged + } else { + throw new EmitterException("expected DocumentStartEvent, but got " + event); + } + } + } + + private class ExpectDocumentEnd implements EmitterState { + public void expect() throws IOException { + event = blockCommentsCollector.collectEventsAndPoll(event); + writeBlockComment(); + if (event instanceof DocumentEndEvent) { + writeIndent(); + if (((DocumentEndEvent) event).getExplicit()) { + writeIndicator("...", true, false, false); + writeIndent(); + } + flushStream(); + state = new ExpectDocumentStart(false); + } else { + throw new EmitterException("expected DocumentEndEvent, but got " + event); + } + } + } + + private class ExpectDocumentRoot implements EmitterState { + public void expect() throws IOException { + event = blockCommentsCollector.collectEventsAndPoll(event); + if (!blockCommentsCollector.isEmpty()) { + writeBlockComment(); + if (event instanceof DocumentEndEvent) { + new ExpectDocumentEnd().expect(); + return; + } + } + states.push(new ExpectDocumentEnd()); + expectNode(true, false, false); + } + } + + // Node handlers. + + private void expectNode(boolean root, boolean mapping, boolean simpleKey) throws IOException { + rootContext = root; + mappingContext = mapping; + simpleKeyContext = simpleKey; + if (event instanceof AliasEvent) { + expectAlias(); + } else if (event instanceof ScalarEvent || event instanceof CollectionStartEvent) { + processAnchor("&"); + processTag(); + if (event instanceof ScalarEvent) { + expectScalar(); + } else if (event instanceof SequenceStartEvent) { + if (flowLevel != 0 || canonical || ((SequenceStartEvent) event).isFlow() + || checkEmptySequence()) { + expectFlowSequence(); + } else { + expectBlockSequence(); + } + } else {// MappingStartEvent + if (flowLevel != 0 || canonical || ((MappingStartEvent) event).isFlow() + || checkEmptyMapping()) { + expectFlowMapping(); + } else { + expectBlockMapping(); + } + } + } else { + throw new EmitterException("expected NodeEvent, but got " + event); + } + } + + private void expectAlias() throws IOException { + if (!(event instanceof AliasEvent)) { + throw new EmitterException("Alias must be provided"); + } + processAnchor("*"); + state = states.pop(); + } + + private void expectScalar() throws IOException { + increaseIndent(true, false); + processScalar(); + indent = indents.pop(); + state = states.pop(); + } + + // Flow sequence handlers. + + private void expectFlowSequence() throws IOException { + writeIndicator("[", true, true, false); + flowLevel++; + increaseIndent(true, false); + if (prettyFlow) { + writeIndent(); + } + state = new ExpectFirstFlowSequenceItem(); + } + + private class ExpectFirstFlowSequenceItem implements EmitterState { + public void expect() throws IOException { + if (event instanceof SequenceEndEvent) { + indent = indents.pop(); + flowLevel--; + writeIndicator("]", false, false, false); + inlineCommentsCollector.collectEvents(); + writeInlineComments(); + state = states.pop(); + } else if (event instanceof CommentEvent) { + blockCommentsCollector.collectEvents(event); + writeBlockComment(); + } else { + if (canonical || (column > bestWidth && splitLines) || prettyFlow) { + writeIndent(); + } + states.push(new ExpectFlowSequenceItem()); + expectNode(false, false, false); + event = inlineCommentsCollector.collectEvents(event); + writeInlineComments(); + } + } + } + + private class ExpectFlowSequenceItem implements EmitterState { + public void expect() throws IOException { + if (event instanceof SequenceEndEvent) { + indent = indents.pop(); + flowLevel--; + if (canonical) { + writeIndicator(",", false, false, false); + writeIndent(); + } else if (prettyFlow) { + writeIndent(); + } + writeIndicator("]", false, false, false); + inlineCommentsCollector.collectEvents(); + writeInlineComments(); + if (prettyFlow) { + writeIndent(); + } + state = states.pop(); + } else if (event instanceof CommentEvent) { + event = blockCommentsCollector.collectEvents(event); + } else { + writeIndicator(",", false, false, false); + writeBlockComment(); + if (canonical || (column > bestWidth && splitLines) || prettyFlow) { + writeIndent(); + } + states.push(new ExpectFlowSequenceItem()); + expectNode(false, false, false); + event = inlineCommentsCollector.collectEvents(event); + writeInlineComments(); + } + } + } + + // Flow mapping handlers. + + private void expectFlowMapping() throws IOException { + writeIndicator("{", true, true, false); + flowLevel++; + increaseIndent(true, false); + if (prettyFlow) { + writeIndent(); + } + state = new ExpectFirstFlowMappingKey(); + } + + private class ExpectFirstFlowMappingKey implements EmitterState { + public void expect() throws IOException { + event = blockCommentsCollector.collectEventsAndPoll(event); + writeBlockComment(); + if (event instanceof MappingEndEvent) { + indent = indents.pop(); + flowLevel--; + writeIndicator("}", false, false, false); + inlineCommentsCollector.collectEvents(); + writeInlineComments(); + state = states.pop(); + } else { + if (canonical || (column > bestWidth && splitLines) || prettyFlow) { + writeIndent(); + } + if (!canonical && checkSimpleKey()) { + states.push(new ExpectFlowMappingSimpleValue()); + expectNode(false, true, true); + } else { + writeIndicator("?", true, false, false); + states.push(new ExpectFlowMappingValue()); + expectNode(false, true, false); + } + } + } + } + + private class ExpectFlowMappingKey implements EmitterState { + public void expect() throws IOException { + if (event instanceof MappingEndEvent) { + indent = indents.pop(); + flowLevel--; + if (canonical) { + writeIndicator(",", false, false, false); + writeIndent(); + } + if (prettyFlow) { + writeIndent(); + } + writeIndicator("}", false, false, false); + inlineCommentsCollector.collectEvents(); + writeInlineComments(); + state = states.pop(); + } else { + writeIndicator(",", false, false, false); + event = blockCommentsCollector.collectEventsAndPoll(event); + writeBlockComment(); + if (canonical || (column > bestWidth && splitLines) || prettyFlow) { + writeIndent(); + } + if (!canonical && checkSimpleKey()) { + states.push(new ExpectFlowMappingSimpleValue()); + expectNode(false, true, true); + } else { + writeIndicator("?", true, false, false); + states.push(new ExpectFlowMappingValue()); + expectNode(false, true, false); + } + } + } + } + + private class ExpectFlowMappingSimpleValue implements EmitterState { + public void expect() throws IOException { + writeIndicator(":", false, false, false); + event = inlineCommentsCollector.collectEventsAndPoll(event); + writeInlineComments(); + states.push(new ExpectFlowMappingKey()); + expectNode(false, true, false); + inlineCommentsCollector.collectEvents(event); + writeInlineComments(); + } + } + + private class ExpectFlowMappingValue implements EmitterState { + public void expect() throws IOException { + if (canonical || (column > bestWidth) || prettyFlow) { + writeIndent(); + } + writeIndicator(":", true, false, false); + event = inlineCommentsCollector.collectEventsAndPoll(event); + writeInlineComments(); + states.push(new ExpectFlowMappingKey()); + expectNode(false, true, false); + inlineCommentsCollector.collectEvents(event); + writeInlineComments(); + } + } + + // Block sequence handlers. + + private void expectBlockSequence() throws IOException { + boolean indentless = mappingContext && !indention; + increaseIndent(false, indentless); + state = new ExpectFirstBlockSequenceItem(); + } + + private class ExpectFirstBlockSequenceItem implements EmitterState { + public void expect() throws IOException { + new ExpectBlockSequenceItem(true).expect(); + } + } + + private class ExpectBlockSequenceItem implements EmitterState { + private final boolean first; + + public ExpectBlockSequenceItem(boolean first) { + this.first = first; + } + + public void expect() throws IOException { + if (!this.first && event instanceof SequenceEndEvent) { + indent = indents.pop(); + state = states.pop(); + } else if( event instanceof CommentEvent) { + blockCommentsCollector.collectEvents(event); + } else { + writeIndent(); + if (!indentWithIndicator || this.first) { + writeWhitespace(indicatorIndent); + } + writeIndicator("-", true, false, true); + if (indentWithIndicator && this.first) { + indent += indicatorIndent; + } + if (!blockCommentsCollector.isEmpty()) { + increaseIndent(false, false); + writeBlockComment(); + if(event instanceof ScalarEvent) { + analysis = analyzeScalar(((ScalarEvent)event).getValue()); + if (!analysis.isEmpty()) { + writeIndent(); + } + } + indent = indents.pop(); + } + states.push(new ExpectBlockSequenceItem(false)); + expectNode(false, false, false); + inlineCommentsCollector.collectEvents(); + writeInlineComments(); + } + } + } + + // Block mapping handlers. + private void expectBlockMapping() throws IOException { + increaseIndent(false, false); + state = new ExpectFirstBlockMappingKey(); + } + + private class ExpectFirstBlockMappingKey implements EmitterState { + public void expect() throws IOException { + new ExpectBlockMappingKey(true).expect(); + } + } + + private class ExpectBlockMappingKey implements EmitterState { + private final boolean first; + + public ExpectBlockMappingKey(boolean first) { + this.first = first; + } + + public void expect() throws IOException { + event = blockCommentsCollector.collectEventsAndPoll(event); + writeBlockComment(); + if (!this.first && event instanceof MappingEndEvent) { + indent = indents.pop(); + state = states.pop(); + } else { + writeIndent(); + if (checkSimpleKey()) { + states.push(new ExpectBlockMappingSimpleValue()); + expectNode(false, true, true); + } else { + writeIndicator("?", true, false, true); + states.push(new ExpectBlockMappingValue()); + expectNode(false, true, false); + } + } + } + } + + private boolean isFoldedOrLiteral(Event event) { + if(!event.is(ID.Scalar)) { + return false; + } + ScalarEvent scalarEvent = (ScalarEvent) event; + ScalarStyle style = scalarEvent.getScalarStyle(); + return style == ScalarStyle.FOLDED || style == ScalarStyle.LITERAL; + } + + private class ExpectBlockMappingSimpleValue implements EmitterState { + public void expect() throws IOException { + writeIndicator(":", false, false, false); + event = inlineCommentsCollector.collectEventsAndPoll(event); + if(!isFoldedOrLiteral(event)) { + if(writeInlineComments()) { + increaseIndent(true, false); + writeIndent(); + indent = indents.pop(); + } + } + event = blockCommentsCollector.collectEventsAndPoll(event); + if(!blockCommentsCollector.isEmpty()) { + increaseIndent(true, false); + writeBlockComment(); + writeIndent(); + indent = indents.pop(); + } + states.push(new ExpectBlockMappingKey(false)); + expectNode(false, true, false); + inlineCommentsCollector.collectEvents(); + writeInlineComments(); + } + } + + private class ExpectBlockMappingValue implements EmitterState { + public void expect() throws IOException { + writeIndent(); + writeIndicator(":", true, false, true); + event = inlineCommentsCollector.collectEventsAndPoll(event); + writeInlineComments(); + event = blockCommentsCollector.collectEventsAndPoll(event); + writeBlockComment(); + states.push(new ExpectBlockMappingKey(false)); + expectNode(false, true, false); + inlineCommentsCollector.collectEvents(event); + writeInlineComments(); + } + } + + // Checkers. + + private boolean checkEmptySequence() { + return event instanceof SequenceStartEvent && !events.isEmpty() && events.peek() instanceof SequenceEndEvent; + } + + private boolean checkEmptyMapping() { + return event instanceof MappingStartEvent && !events.isEmpty() && events.peek() instanceof MappingEndEvent; + } + + private boolean checkEmptyDocument() { + if (!(event instanceof DocumentStartEvent) || events.isEmpty()) { + return false; + } + Event event = events.peek(); + if (event instanceof ScalarEvent) { + ScalarEvent e = (ScalarEvent) event; + return e.getAnchor() == null && e.getTag() == null && e.getImplicit() != null && e + .getValue().length() == 0; + } + return false; + } + + private boolean checkSimpleKey() { + int length = 0; + if (event instanceof NodeEvent && ((NodeEvent) event).getAnchor() != null) { + if (preparedAnchor == null) { + preparedAnchor = prepareAnchor(((NodeEvent) event).getAnchor()); + } + length += preparedAnchor.length(); + } + String tag = null; + if (event instanceof ScalarEvent) { + tag = ((ScalarEvent) event).getTag(); + } else if (event instanceof CollectionStartEvent) { + tag = ((CollectionStartEvent) event).getTag(); + } + if (tag != null) { + if (preparedTag == null) { + preparedTag = prepareTag(tag); + } + length += preparedTag.length(); + } + if (event instanceof ScalarEvent) { + if (analysis == null) { + analysis = analyzeScalar(((ScalarEvent) event).getValue()); + } + length += analysis.getScalar().length(); + } + return length < maxSimpleKeyLength && (event instanceof AliasEvent + || (event instanceof ScalarEvent && !analysis.isEmpty() && !analysis.isMultiline()) + || checkEmptySequence() || checkEmptyMapping()); + } + + // Anchor, Tag, and Scalar processors. + + private void processAnchor(String indicator) throws IOException { + NodeEvent ev = (NodeEvent) event; + if (ev.getAnchor() == null) { + preparedAnchor = null; + return; + } + if (preparedAnchor == null) { + preparedAnchor = prepareAnchor(ev.getAnchor()); + } + writeIndicator(indicator + preparedAnchor, true, false, false); + preparedAnchor = null; + } + + private void processTag() throws IOException { + String tag = null; + if (event instanceof ScalarEvent) { + ScalarEvent ev = (ScalarEvent) event; + tag = ev.getTag(); + if (style == null) { + style = chooseScalarStyle(); + } + if ((!canonical || tag == null) && ((style == null && ev.getImplicit() + .canOmitTagInPlainScalar()) || (style != null && ev.getImplicit() + .canOmitTagInNonPlainScalar()))) { + preparedTag = null; + return; + } + if (ev.getImplicit().canOmitTagInPlainScalar() && tag == null) { + tag = "!"; + preparedTag = null; + } + } else { + CollectionStartEvent ev = (CollectionStartEvent) event; + tag = ev.getTag(); + if ((!canonical || tag == null) && ev.getImplicit()) { + preparedTag = null; + return; + } + } + if (tag == null) { + throw new EmitterException("tag is not specified"); + } + if (preparedTag == null) { + preparedTag = prepareTag(tag); + } + writeIndicator(preparedTag, true, false, false); + preparedTag = null; + } + + private DumperOptions.ScalarStyle chooseScalarStyle() { + ScalarEvent ev = (ScalarEvent) event; + if (analysis == null) { + analysis = analyzeScalar(ev.getValue()); + } + if (!ev.isPlain() && ev.getScalarStyle() == DumperOptions.ScalarStyle.DOUBLE_QUOTED || this.canonical) { + return DumperOptions.ScalarStyle.DOUBLE_QUOTED; + } + if (ev.isPlain() && ev.getImplicit().canOmitTagInPlainScalar()) { + if (!(simpleKeyContext && (analysis.isEmpty() || analysis.isMultiline())) + && ((flowLevel != 0 && analysis.isAllowFlowPlain()) || (flowLevel == 0 && analysis.isAllowBlockPlain()))) { + return null; + } + } + if (!ev.isPlain() && (ev.getScalarStyle() == DumperOptions.ScalarStyle.LITERAL || ev.getScalarStyle() == DumperOptions.ScalarStyle.FOLDED)) { + if (flowLevel == 0 && !simpleKeyContext && analysis.isAllowBlock()) { + return ev.getScalarStyle(); + } + } + if (ev.isPlain() || ev.getScalarStyle() == DumperOptions.ScalarStyle.SINGLE_QUOTED) { + if (analysis.isAllowSingleQuoted() && !(simpleKeyContext && analysis.isMultiline())) { + return DumperOptions.ScalarStyle.SINGLE_QUOTED; + } + } + return DumperOptions.ScalarStyle.DOUBLE_QUOTED; + } + + private void processScalar() throws IOException { + ScalarEvent ev = (ScalarEvent) event; + if (analysis == null) { + analysis = analyzeScalar(ev.getValue()); + } + if (style == null) { + style = chooseScalarStyle(); + } + boolean split = !simpleKeyContext && splitLines; + if (style == null) { + writePlain(analysis.getScalar(), split); + } else { + switch (style) { + case DOUBLE_QUOTED: + writeDoubleQuoted(analysis.getScalar(), split); + break; + case SINGLE_QUOTED: + writeSingleQuoted(analysis.getScalar(), split); + break; + case FOLDED: + writeFolded(analysis.getScalar(), split); + break; + case LITERAL: + writeLiteral(analysis.getScalar()); + break; + default: + throw new YAMLException("Unexpected style: " + style); + } + } + analysis = null; + style = null; + } + + // Analyzers. + + private String prepareVersion(Version version) { + if (version.major() != 1) { + throw new EmitterException("unsupported YAML version: " + version); + } + return version.getRepresentation(); + } + + private final static Pattern HANDLE_FORMAT = Pattern.compile("^![-_\\w]*!$"); + + private String prepareTagHandle(String handle) { + if (handle.length() == 0) { + throw new EmitterException("tag handle must not be empty"); + } else if (handle.charAt(0) != '!' || handle.charAt(handle.length() - 1) != '!') { + throw new EmitterException("tag handle must start and end with '!': " + handle); + } else if (!"!".equals(handle) && !HANDLE_FORMAT.matcher(handle).matches()) { + throw new EmitterException("invalid character in the tag handle: " + handle); + } + return handle; + } + + private String prepareTagPrefix(String prefix) { + if (prefix.length() == 0) { + 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, start, end); + } + return chunks.toString(); + } + + private String prepareTag(String tag) { + if (tag.length() == 0) { + 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 : tagPrefixes.keySet()) { + if (tag.startsWith(prefix) && ("!".equals(prefix) || prefix.length() < tag.length())) { + handle = prefix; + } + } + if (handle != null) { + suffix = tag.substring(handle.length()); + handle = tagPrefixes.get(handle); + } + + int end = suffix.length(); + String suffixText = end > 0 ? suffix.substring(0, end) : ""; + + if (handle != null) { + return handle + suffixText; + } + return "!<" + suffixText + ">"; + } + + static String prepareAnchor(String anchor) { + if (anchor.length() == 0) { + throw new EmitterException("anchor must not be empty"); + } + for (Character invalid : INVALID_ANCHOR) { + if (anchor.indexOf(invalid) > -1) { + throw new EmitterException("Invalid character '" + invalid + "' in the anchor: " + anchor); + } + } + Matcher matcher = SPACES_PATTERN.matcher(anchor); + if (matcher.find()) { + throw new EmitterException("Anchor may not contain spaces: " + anchor); + } + return anchor; + } + + private ScalarAnalysis analyzeScalar(String scalar) { + // Empty scalar is a special case. + if (scalar.length() == 0) { + 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 preceededByWhitespace = true; + boolean followedByWhitespace = scalar.length() == 1 || Constant.NULL_BL_T_LINEBR.has(scalar.codePointAt(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()) { + int c = scalar.codePointAt(index); + // Check for indicators. + if (index == 0) { + // Leading indicators are special characters. + if ("#,[]{}&*!|>'\"%@`".indexOf(c) != -1) { + flowIndicators = true; + blockIndicators = true; + } + if (c == '?' || c == ':') { + flowIndicators = true; + if (followedByWhitespace) { + blockIndicators = true; + } + } + if (c == '-' && followedByWhitespace) { + flowIndicators = true; + blockIndicators = true; + } + } else { + // Some indicators cannot appear within a scalar as well. + if (",?[]{}".indexOf(c) != -1) { + flowIndicators = true; + } + if (c == ':') { + flowIndicators = true; + if (followedByWhitespace) { + blockIndicators = true; + } + } + if (c == '#' && preceededByWhitespace) { + flowIndicators = true; + blockIndicators = true; + } + } + // Check for line breaks, special, and unicode characters. + boolean isLineBreak = Constant.LINEBR.has(c); + if (isLineBreak) { + lineBreaks = true; + } + if (!(c == '\n' || (0x20 <= c && c <= 0x7E))) { + if (c == 0x85 || (c >= 0xA0 && c <= 0xD7FF) + || (c >= 0xE000 && c <= 0xFFFD) + || (c >= 0x10000 && c <= 0x10FFFF)) { + // unicode is used + if (!this.allowUnicode) { + specialCharacters = true; + } + } else { + specialCharacters = true; + } + } + // Detect important whitespace combinations. + if (c == ' ') { + 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 += Character.charCount(c); + preceededByWhitespace = Constant.NULL_BL_T.has(c) || isLineBreak; + followedByWhitespace = true; + if (index + 1 < scalar.length()) { + int nextIndex = index + Character.charCount(scalar.codePointAt(index)); + if (nextIndex < scalar.length()) { + followedByWhitespace = (Constant.NULL_BL_T.has(scalar.codePointAt(nextIndex))) || 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 { + stream.flush(); + } + + void writeStreamStart() { + // BOM is written by Writer. + } + + void writeStreamEnd() throws IOException { + flushStream(); + } + + void writeIndicator(String indicator, boolean needWhitespace, boolean whitespace, + boolean indentation) throws IOException { + if (!this.whitespace && needWhitespace) { + this.column++; + stream.write(SPACE); + } + this.whitespace = whitespace; + this.indention = this.indention && indentation; + this.column += indicator.length(); + openEnded = false; + stream.write(indicator); + } + + void writeIndent() throws IOException { + int indent; + if (this.indent != null) { + indent = this.indent; + } else { + indent = 0; + } + + if (!this.indention || this.column > indent || (this.column == indent && !this.whitespace)) { + writeLineBreak(null); + } + + writeWhitespace(indent - this.column); + } + + private 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; + stream.write(data); + } + + private void writeLineBreak(String data) throws IOException { + this.whitespace = true; + this.indention = true; + this.column = 0; + if (data == null) { + stream.write(this.bestLineBreak); + } else { + stream.write(data); + } + } + + void writeVersionDirective(String versionText) throws IOException { + stream.write("%YAML "); + stream.write(versionText); + writeLineBreak(null); + } + + void writeTagDirective(String handleText, String prefixText) throws IOException { + // XXX: not sure 4 invocations better then StringBuilders created by str + // + str + stream.write("%TAG "); + stream.write(handleText); + stream.write(SPACE); + stream.write(prefixText); + writeLineBreak(null); + } + + // Scalar streams. + private void writeSingleQuoted(String text, boolean split) throws IOException { + 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()) { + writeIndent(); + } else { + int len = end - start; + this.column += len; + stream.write(text, start, len); + } + start = end; + } + } else if (breaks) { + if (ch == 0 || Constant.LINEBR.hasNo(ch)) { + if (text.charAt(start) == '\n') { + writeLineBreak(null); + } + String data = text.substring(start, end); + for (char br : data.toCharArray()) { + if (br == '\n') { + writeLineBreak(null); + } else { + writeLineBreak(String.valueOf(br)); + } + } + writeIndent(); + start = end; + } + } else { + if (Constant.LINEBR.has(ch, "\0 '")) { + if (start < end) { + int len = end - start; + this.column += len; + stream.write(text, start, len); + start = end; + } + } + } + if (ch == '\'') { + this.column += 2; + stream.write("''"); + start = end + 1; + } + if (ch != 0) { + spaces = ch == ' '; + breaks = Constant.LINEBR.has(ch); + } + end++; + } + writeIndicator("'", false, false, false); + } + + private void writeDoubleQuoted(String text, boolean split) throws IOException { + 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; + 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, 16); + 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, 16); + data = "\\u" + s.substring(s.length() - 4); + } + } else { + String s = "000" + Integer.toString(ch, 16); + data = "\\u" + s.substring(s.length() - 4); + } + } else { + data = String.valueOf(ch); + } + this.column += data.length(); + stream.write(data); + start = end + 1; + } + } + if ((0 < end && end < (text.length() - 1)) && (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(); + stream.write(data); + writeIndent(); + this.whitespace = false; + this.indention = false; + if (text.charAt(start) == ' ') { + data = "\\"; + this.column += data.length(); + stream.write(data); + } + } + end += 1; + } + writeIndicator("\"", false, false, false); + } + + private boolean writeCommentLines(List commentLines) throws IOException { + boolean wroteComment = false; + if(emitComments) { + int indentColumns = 0; + boolean firstComment = true; + for (CommentLine commentLine : commentLines) { + if (commentLine.getCommentType() != CommentType.BLANK_LINE) { + if (firstComment) { + firstComment = false; + writeIndicator("#", commentLine.getCommentType() == CommentType.IN_LINE, false, false); + indentColumns = this.column > 0 ? this.column - 1 : 0; + } else { + writeWhitespace(indentColumns); + writeIndicator("#", false, false, false); + } + stream.write(commentLine.getValue()); + writeLineBreak(null); + } else { + writeLineBreak(null); + writeIndent(); + } + wroteComment = true; + } + } + return wroteComment; + } + + private void writeBlockComment() throws IOException { + if(!blockCommentsCollector.isEmpty()) { + writeIndent(); + writeCommentLines(blockCommentsCollector.consume()); + } + } + + private boolean writeInlineComments() throws IOException { + return writeCommentLines(inlineCommentsCollector.consume()); + } + + private String determineBlockHints(String text) { + StringBuilder hints = new StringBuilder(); + if (Constant.LINEBR.has(text.charAt(0), " ")) { + hints.append(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(); + } + + void writeFolded(String text, boolean split) throws IOException { + String hints = determineBlockHints(text); + writeIndicator(">" + hints, true, false, false); + if (hints.length() > 0 && (hints.charAt(hints.length() - 1) == '+')) { + openEnded = true; + } + if(!writeInlineComments()) { + 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') { + writeLineBreak(null); + } + leadingSpace = ch == ' '; + String data = text.substring(start, end); + for (char br : data.toCharArray()) { + if (br == '\n') { + writeLineBreak(null); + } else { + writeLineBreak(String.valueOf(br)); + } + } + if (ch != 0) { + writeIndent(); + } + start = end; + } + } else if (spaces) { + if (ch != ' ') { + if (start + 1 == end && this.column > this.bestWidth && split) { + writeIndent(); + } else { + int len = end - start; + this.column += len; + stream.write(text, start, len); + } + start = end; + } + } else { + if (Constant.LINEBR.has(ch, "\0 ")) { + int len = end - start; + this.column += len; + stream.write(text, start, len); + if (ch == 0) { + writeLineBreak(null); + } + start = end; + } + } + if (ch != 0) { + breaks = Constant.LINEBR.has(ch); + spaces = ch == ' '; + } + end++; + } + } + + void writeLiteral(String text) throws IOException { + String hints = determineBlockHints(text); + writeIndicator("|" + hints, true, false, false); + if (hints.length() > 0 && (hints.charAt(hints.length() - 1)) == '+') { + openEnded = true; + } + if(!writeInlineComments()) { + 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') { + writeLineBreak(null); + } else { + writeLineBreak(String.valueOf(br)); + } + } + if (ch != 0) { + writeIndent(); + } + start = end; + } + } else { + if (ch == 0 || Constant.LINEBR.has(ch)) { + stream.write(text, start, end - start); + if (ch == 0) { + writeLineBreak(null); + } + start = end; + } + } + if (ch != 0) { + breaks = Constant.LINEBR.has(ch); + } + end++; + } + } + + void writePlain(String text, boolean split) throws IOException { + if (rootContext) { + openEnded = true; + } + if (text.length() == 0) { + return; + } + if (!this.whitespace) { + this.column++; + 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) { + writeIndent(); + this.whitespace = false; + this.indention = false; + } else { + int len = end - start; + this.column += len; + stream.write(text, start, len); + } + start = end; + } + } else if (breaks) { + if (Constant.LINEBR.hasNo(ch)) { + if (text.charAt(start) == '\n') { + writeLineBreak(null); + } + String data = text.substring(start, end); + for (char br : data.toCharArray()) { + if (br == '\n') { + writeLineBreak(null); + } else { + writeLineBreak(String.valueOf(br)); + } + } + writeIndent(); + this.whitespace = false; + this.indention = false; + start = end; + } + } else { + if (Constant.LINEBR.has(ch, "\0 ")) { + int len = end - start; + this.column += len; + stream.write(text, start, len); + start = end; + } + } + if (ch != 0) { + spaces = ch == ' '; + breaks = Constant.LINEBR.has(ch); + } + end++; + } + } +}