View Javadoc

1   /**
2    * Copyright (c) 2008-2011, http://www.snakeyaml.org
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.yaml.snakeyaml.emitter;
18  
19  import java.io.IOException;
20  import java.io.Writer;
21  import java.util.HashMap;
22  import java.util.Iterator;
23  import java.util.LinkedHashMap;
24  import java.util.Map;
25  import java.util.Queue;
26  import java.util.Set;
27  import java.util.TreeSet;
28  import java.util.concurrent.ArrayBlockingQueue;
29  import java.util.regex.Pattern;
30  
31  import org.yaml.snakeyaml.DumperOptions;
32  import org.yaml.snakeyaml.DumperOptions.ScalarStyle;
33  import org.yaml.snakeyaml.events.AliasEvent;
34  import org.yaml.snakeyaml.events.CollectionEndEvent;
35  import org.yaml.snakeyaml.events.CollectionStartEvent;
36  import org.yaml.snakeyaml.events.DocumentEndEvent;
37  import org.yaml.snakeyaml.events.DocumentStartEvent;
38  import org.yaml.snakeyaml.events.Event;
39  import org.yaml.snakeyaml.events.MappingEndEvent;
40  import org.yaml.snakeyaml.events.MappingStartEvent;
41  import org.yaml.snakeyaml.events.NodeEvent;
42  import org.yaml.snakeyaml.events.ScalarEvent;
43  import org.yaml.snakeyaml.events.SequenceEndEvent;
44  import org.yaml.snakeyaml.events.SequenceStartEvent;
45  import org.yaml.snakeyaml.events.StreamEndEvent;
46  import org.yaml.snakeyaml.events.StreamStartEvent;
47  import org.yaml.snakeyaml.nodes.Tag;
48  import org.yaml.snakeyaml.scanner.Constant;
49  import org.yaml.snakeyaml.util.ArrayStack;
50  
51  /**
52   * <pre>
53   * Emitter expects events obeying the following grammar:
54   * stream ::= STREAM-START document* STREAM-END
55   * document ::= DOCUMENT-START node DOCUMENT-END
56   * node ::= SCALAR | sequence | mapping
57   * sequence ::= SEQUENCE-START node* SEQUENCE-END
58   * mapping ::= MAPPING-START (node node)* MAPPING-END
59   * </pre>
60   */
61  public final class Emitter implements Emitable {
62      private static final Map<Character, String> ESCAPE_REPLACEMENTS = new HashMap<Character, String>();
63      public static final int MIN_INDENT = 1;
64      public static final int MAX_INDENT = 10;
65  
66      public static final char[] SPACE = { ' ' };
67  
68      static {
69          ESCAPE_REPLACEMENTS.put(new Character('\0'), "0");
70          ESCAPE_REPLACEMENTS.put(new Character('\u0007'), "a");
71          ESCAPE_REPLACEMENTS.put(new Character('\u0008'), "b");
72          ESCAPE_REPLACEMENTS.put(new Character('\u0009'), "t");
73          ESCAPE_REPLACEMENTS.put(new Character('\n'), "n");
74          ESCAPE_REPLACEMENTS.put(new Character('\u000B'), "v");
75          ESCAPE_REPLACEMENTS.put(new Character('\u000C'), "f");
76          ESCAPE_REPLACEMENTS.put(new Character('\r'), "r");
77          ESCAPE_REPLACEMENTS.put(new Character('\u001B'), "e");
78          ESCAPE_REPLACEMENTS.put(new Character('"'), "\"");
79          ESCAPE_REPLACEMENTS.put(new Character('\\'), "\\");
80          ESCAPE_REPLACEMENTS.put(new Character('\u0085'), "N");
81          ESCAPE_REPLACEMENTS.put(new Character('\u00A0'), "_");
82          ESCAPE_REPLACEMENTS.put(new Character('\u2028'), "L");
83          ESCAPE_REPLACEMENTS.put(new Character('\u2029'), "P");
84      }
85  
86      private final static Map<String, String> DEFAULT_TAG_PREFIXES = new LinkedHashMap<String, String>();
87      static {
88          DEFAULT_TAG_PREFIXES.put("!", "!");
89          DEFAULT_TAG_PREFIXES.put(Tag.PREFIX, "!!");
90      }
91      // The stream should have the methods `write` and possibly `flush`.
92      private final Writer stream;
93  
94      // Encoding is defined by Writer (cannot be overriden by STREAM-START.)
95      // private Charset encoding;
96  
97      // Emitter is a state machine with a stack of states to handle nested
98      // structures.
99      private final ArrayStack<EmitterState> states;
100     private EmitterState state;
101 
102     // Current event and the event queue.
103     private final Queue<Event> events;
104     private Event event;
105 
106     // The current indentation level and the stack of previous indents.
107     private final ArrayStack<Integer> indents;
108     private Integer indent;
109 
110     // Flow level.
111     private int flowLevel;
112 
113     // Contexts.
114     private boolean rootContext;
115     private boolean mappingContext;
116     private boolean simpleKeyContext;
117 
118     //
119     // Characteristics of the last emitted character:
120     // - current position.
121     // - is it a whitespace?
122     // - is it an indention character
123     // (indentation space, '-', '?', or ':')?
124     // private int line; this variable is not used
125     private int column;
126     private boolean whitespace;
127     private boolean indention;
128     private boolean openEnded;
129 
130     // Formatting details.
131     private Boolean canonical;
132     // pretty print flow by adding extra line breaks
133     private Boolean prettyFlow;
134 
135     private boolean allowUnicode;
136     private int bestIndent;
137     private int bestWidth;
138     private char[] bestLineBreak;
139 
140     // Tag prefixes.
141     private Map<String, String> tagPrefixes;
142 
143     // Prepared anchor and tag.
144     private String preparedAnchor;
145     private String preparedTag;
146 
147     // Scalar analysis and style.
148     private ScalarAnalysis analysis;
149     private Character style;
150     private DumperOptions options;
151 
152     public Emitter(Writer stream, DumperOptions opts) {
153         // The stream should have the methods `write` and possibly `flush`.
154         this.stream = stream;
155         // Emitter is a state machine with a stack of states to handle nested
156         // structures.
157         this.states = new ArrayStack<EmitterState>(100);
158         this.state = new ExpectStreamStart();
159         // Current event and the event queue.
160         this.events = new ArrayBlockingQueue<Event>(100);
161         this.event = null;
162         // The current indentation level and the stack of previous indents.
163         this.indents = new ArrayStack<Integer>(10);
164         this.indent = null;
165         // Flow level.
166         this.flowLevel = 0;
167         // Contexts.
168         mappingContext = false;
169         simpleKeyContext = false;
170 
171         //
172         // Characteristics of the last emitted character:
173         // - current position.
174         // - is it a whitespace?
175         // - is it an indention character
176         // (indentation space, '-', '?', or ':')?
177         column = 0;
178         whitespace = true;
179         indention = true;
180 
181         // Whether the document requires an explicit document indicator
182         openEnded = false;
183 
184         // Formatting details.
185         this.canonical = opts.isCanonical();
186         this.prettyFlow = opts.isPrettyFlow();
187         this.allowUnicode = opts.isAllowUnicode();
188         this.bestIndent = 2;
189         if ((opts.getIndent() > MIN_INDENT) && (opts.getIndent() < MAX_INDENT)) {
190             this.bestIndent = opts.getIndent();
191         }
192         this.bestWidth = 80;
193         if (opts.getWidth() > this.bestIndent * 2) {
194             this.bestWidth = opts.getWidth();
195         }
196         this.bestLineBreak = opts.getLineBreak().getString().toCharArray();
197 
198         // Tag prefixes.
199         this.tagPrefixes = new LinkedHashMap<String, String>();
200 
201         // Prepared anchor and tag.
202         this.preparedAnchor = null;
203         this.preparedTag = null;
204 
205         // Scalar analysis and style.
206         this.analysis = null;
207         this.style = null;
208         this.options = opts;
209     }
210 
211     public void emit(Event event) throws IOException {
212         this.events.add(event);
213         while (!needMoreEvents()) {
214             this.event = this.events.poll();
215             this.state.expect();
216             this.event = null;
217         }
218     }
219 
220     // In some cases, we wait for a few next events before emitting.
221 
222     private boolean needMoreEvents() {
223         if (events.isEmpty()) {
224             return true;
225         }
226         Event event = events.peek();
227         if (event instanceof DocumentStartEvent) {
228             return needEvents(1);
229         } else if (event instanceof SequenceStartEvent) {
230             return needEvents(2);
231         } else if (event instanceof MappingStartEvent) {
232             return needEvents(3);
233         } else {
234             return false;
235         }
236     }
237 
238     private boolean needEvents(int count) {
239         int level = 0;
240         Iterator<Event> iter = events.iterator();
241         iter.next();
242         while (iter.hasNext()) {
243             Event event = iter.next();
244             if (event instanceof DocumentStartEvent || event instanceof CollectionStartEvent) {
245                 level++;
246             } else if (event instanceof DocumentEndEvent || event instanceof CollectionEndEvent) {
247                 level--;
248             } else if (event instanceof StreamEndEvent) {
249                 level = -1;
250             }
251             if (level < 0) {
252                 return false;
253             }
254         }
255         return events.size() < count + 1;
256     }
257 
258     private void increaseIndent(boolean flow, boolean indentless) {
259         indents.push(indent);
260         if (indent == null) {
261             if (flow) {
262                 indent = bestIndent;
263             } else {
264                 indent = 0;
265             }
266         } else if (!indentless) {
267             this.indent += bestIndent;
268         }
269     }
270 
271     // States
272 
273     // Stream handlers.
274 
275     private class ExpectStreamStart implements EmitterState {
276         public void expect() throws IOException {
277             if (event instanceof StreamStartEvent) {
278                 writeStreamStart();
279                 state = new ExpectFirstDocumentStart();
280             } else {
281                 throw new EmitterException("expected StreamStartEvent, but got " + event);
282             }
283         }
284     }
285 
286     private class ExpectNothing implements EmitterState {
287         public void expect() throws IOException {
288             throw new EmitterException("expecting nothing, but got " + event);
289         }
290     }
291 
292     // Document handlers.
293 
294     private class ExpectFirstDocumentStart implements EmitterState {
295         public void expect() throws IOException {
296             new ExpectDocumentStart(true).expect();
297         }
298     }
299 
300     private class ExpectDocumentStart implements EmitterState {
301         private boolean first;
302 
303         public ExpectDocumentStart(boolean first) {
304             this.first = first;
305         }
306 
307         public void expect() throws IOException {
308             if (event instanceof DocumentStartEvent) {
309                 DocumentStartEvent ev = (DocumentStartEvent) event;
310                 if ((ev.getVersion() != null || ev.getTags() != null) && openEnded) {
311                     writeIndicator("...", true, false, false);
312                     writeIndent();
313                 }
314                 if (ev.getVersion() != null) {
315                     String versionText = prepareVersion(ev.getVersion());
316                     writeVersionDirective(versionText);
317                 }
318                 tagPrefixes = new LinkedHashMap<String, String>(DEFAULT_TAG_PREFIXES);
319                 if (ev.getTags() != null) {
320                     Set<String> handles = new TreeSet<String>(ev.getTags().keySet());
321                     for (String handle : handles) {
322                         String prefix = ev.getTags().get(handle);
323                         tagPrefixes.put(prefix, handle);
324                         String handleText = prepareTagHandle(handle);
325                         String prefixText = prepareTagPrefix(prefix);
326                         writeTagDirective(handleText, prefixText);
327                     }
328                 }
329                 boolean implicit = first && !ev.getExplicit() && !canonical
330                         && ev.getVersion() == null && ev.getTags() == null && !checkEmptyDocument();
331                 if (!implicit) {
332                     writeIndent();
333                     writeIndicator("---", true, false, false);
334                     if (canonical) {
335                         writeIndent();
336                     }
337                 }
338                 state = new ExpectDocumentRoot();
339             } else if (event instanceof StreamEndEvent) {
340                 // TODO fix 313 PyYAML changeset
341                 // if (openEnded) {
342                 // writeIndicator("...", true, false, false);
343                 // writeIndent();
344                 // }
345                 writeStreamEnd();
346                 state = new ExpectNothing();
347             } else {
348                 throw new EmitterException("expected DocumentStartEvent, but got " + event);
349             }
350         }
351     }
352 
353     private class ExpectDocumentEnd implements EmitterState {
354         public void expect() throws IOException {
355             if (event instanceof DocumentEndEvent) {
356                 writeIndent();
357                 if (((DocumentEndEvent) event).getExplicit()) {
358                     writeIndicator("...", true, false, false);
359                     writeIndent();
360                 }
361                 flushStream();
362                 state = new ExpectDocumentStart(false);
363             } else {
364                 throw new EmitterException("expected DocumentEndEvent, but got " + event);
365             }
366         }
367     }
368 
369     private class ExpectDocumentRoot implements EmitterState {
370         public void expect() throws IOException {
371             states.push(new ExpectDocumentEnd());
372             expectNode(true, false, false, false);
373         }
374     }
375 
376     // Node handlers.
377 
378     private void expectNode(boolean root, boolean sequence, boolean mapping, boolean simpleKey)
379             throws IOException {
380         rootContext = root;
381         mappingContext = mapping;
382         simpleKeyContext = simpleKey;
383         if (event instanceof AliasEvent) {
384             expectAlias();
385         } else if (event instanceof ScalarEvent || event instanceof CollectionStartEvent) {
386             processAnchor("&");
387             processTag();
388             if (event instanceof ScalarEvent) {
389                 expectScalar();
390             } else if (event instanceof SequenceStartEvent) {
391                 if (flowLevel != 0 || canonical || ((SequenceStartEvent) event).getFlowStyle()
392                         || checkEmptySequence()) {
393                     expectFlowSequence();
394                 } else {
395                     expectBlockSequence();
396                 }
397             } else {// MappingStartEvent
398                 if (flowLevel != 0 || canonical || ((MappingStartEvent) event).getFlowStyle()
399                         || checkEmptyMapping()) {
400                     expectFlowMapping();
401                 } else {
402                     expectBlockMapping();
403                 }
404             }
405         } else {
406             throw new EmitterException("expected NodeEvent, but got " + event);
407         }
408     }
409 
410     private void expectAlias() throws IOException {
411         if (((NodeEvent) event).getAnchor() == null) {
412             throw new EmitterException("anchor is not specified for alias");
413         }
414         processAnchor("*");
415         state = states.pop();
416     }
417 
418     private void expectScalar() throws IOException {
419         increaseIndent(true, false);
420         processScalar();
421         indent = indents.pop();
422         state = states.pop();
423     }
424 
425     // Flow sequence handlers.
426 
427     private void expectFlowSequence() throws IOException {
428         writeIndicator("[", true, true, false);
429         flowLevel++;
430         increaseIndent(true, false);
431         if (prettyFlow) {
432             writeIndent();
433         }
434         state = new ExpectFirstFlowSequenceItem();
435     }
436 
437     private class ExpectFirstFlowSequenceItem implements EmitterState {
438         public void expect() throws IOException {
439             if (event instanceof SequenceEndEvent) {
440                 indent = indents.pop();
441                 flowLevel--;
442                 writeIndicator("]", false, false, false);
443                 state = states.pop();
444             } else {
445                 if (canonical || column > bestWidth || prettyFlow) {
446                     writeIndent();
447                 }
448                 states.push(new ExpectFlowSequenceItem());
449                 expectNode(false, true, false, false);
450             }
451         }
452     }
453 
454     private class ExpectFlowSequenceItem implements EmitterState {
455         public void expect() throws IOException {
456             if (event instanceof SequenceEndEvent) {
457                 indent = indents.pop();
458                 flowLevel--;
459                 if (canonical) {
460                     writeIndicator(",", false, false, false);
461                     writeIndent();
462                 }
463                 writeIndicator("]", false, false, false);
464                 if (prettyFlow) {
465                     writeIndent();
466                 }
467                 state = states.pop();
468             } else {
469                 writeIndicator(",", false, false, false);
470                 if (canonical || column > bestWidth || prettyFlow) {
471                     writeIndent();
472                 }
473                 states.push(new ExpectFlowSequenceItem());
474                 expectNode(false, true, false, false);
475             }
476         }
477     }
478 
479     // Flow mapping handlers.
480 
481     private void expectFlowMapping() throws IOException {
482         writeIndicator("{", true, true, false);
483         flowLevel++;
484         increaseIndent(true, false);
485         if (prettyFlow) {
486             writeIndent();
487         }
488         state = new ExpectFirstFlowMappingKey();
489     }
490 
491     private class ExpectFirstFlowMappingKey implements EmitterState {
492         public void expect() throws IOException {
493             if (event instanceof MappingEndEvent) {
494                 indent = indents.pop();
495                 flowLevel--;
496                 writeIndicator("}", false, false, false);
497                 state = states.pop();
498             } else {
499                 if (canonical || column > bestWidth || prettyFlow) {
500                     writeIndent();
501                 }
502                 if (!canonical && checkSimpleKey()) {
503                     states.push(new ExpectFlowMappingSimpleValue());
504                     expectNode(false, false, true, true);
505                 } else {
506                     writeIndicator("?", true, false, false);
507                     states.push(new ExpectFlowMappingValue());
508                     expectNode(false, false, true, false);
509                 }
510             }
511         }
512     }
513 
514     private class ExpectFlowMappingKey implements EmitterState {
515         public void expect() throws IOException {
516             if (event instanceof MappingEndEvent) {
517                 indent = indents.pop();
518                 flowLevel--;
519                 if (canonical) {
520                     writeIndicator(",", false, false, false);
521                     writeIndent();
522                 }
523                 if (prettyFlow) {
524                     writeIndent();
525                 }
526                 writeIndicator("}", false, false, false);
527                 state = states.pop();
528             } else {
529                 writeIndicator(",", false, false, false);
530                 if (canonical || column > bestWidth || prettyFlow) {
531                     writeIndent();
532                 }
533                 if (!canonical && checkSimpleKey()) {
534                     states.push(new ExpectFlowMappingSimpleValue());
535                     expectNode(false, false, true, true);
536                 } else {
537                     writeIndicator("?", true, false, false);
538                     states.push(new ExpectFlowMappingValue());
539                     expectNode(false, false, true, false);
540                 }
541             }
542         }
543     }
544 
545     private class ExpectFlowMappingSimpleValue implements EmitterState {
546         public void expect() throws IOException {
547             writeIndicator(":", false, false, false);
548             states.push(new ExpectFlowMappingKey());
549             expectNode(false, false, true, false);
550         }
551     }
552 
553     private class ExpectFlowMappingValue implements EmitterState {
554         public void expect() throws IOException {
555             if (canonical || column > bestWidth || prettyFlow) {
556                 writeIndent();
557             }
558             writeIndicator(":", true, false, false);
559             states.push(new ExpectFlowMappingKey());
560             expectNode(false, false, true, false);
561         }
562     }
563 
564     // Block sequence handlers.
565 
566     private void expectBlockSequence() throws IOException {
567         boolean indentless = (mappingContext && !indention);
568         increaseIndent(false, indentless);
569         state = new ExpectFirstBlockSequenceItem();
570     }
571 
572     private class ExpectFirstBlockSequenceItem implements EmitterState {
573         public void expect() throws IOException {
574             new ExpectBlockSequenceItem(true).expect();
575         }
576     }
577 
578     private class ExpectBlockSequenceItem implements EmitterState {
579         private boolean first;
580 
581         public ExpectBlockSequenceItem(boolean first) {
582             this.first = first;
583         }
584 
585         public void expect() throws IOException {
586             if (!this.first && event instanceof SequenceEndEvent) {
587                 indent = indents.pop();
588                 state = states.pop();
589             } else {
590                 writeIndent();
591                 writeIndicator("-", true, false, true);
592                 states.push(new ExpectBlockSequenceItem(false));
593                 expectNode(false, true, false, false);
594             }
595         }
596     }
597 
598     // Block mapping handlers.
599     private void expectBlockMapping() throws IOException {
600         increaseIndent(false, false);
601         state = new ExpectFirstBlockMappingKey();
602     }
603 
604     private class ExpectFirstBlockMappingKey implements EmitterState {
605         public void expect() throws IOException {
606             new ExpectBlockMappingKey(true).expect();
607         }
608     }
609 
610     private class ExpectBlockMappingKey implements EmitterState {
611         private boolean first;
612 
613         public ExpectBlockMappingKey(boolean first) {
614             this.first = first;
615         }
616 
617         public void expect() throws IOException {
618             if (!this.first && event instanceof MappingEndEvent) {
619                 indent = indents.pop();
620                 state = states.pop();
621             } else {
622                 writeIndent();
623                 if (checkSimpleKey()) {
624                     states.push(new ExpectBlockMappingSimpleValue());
625                     expectNode(false, false, true, true);
626                 } else {
627                     writeIndicator("?", true, false, true);
628                     states.push(new ExpectBlockMappingValue());
629                     expectNode(false, false, true, false);
630                 }
631             }
632         }
633     }
634 
635     private class ExpectBlockMappingSimpleValue implements EmitterState {
636         public void expect() throws IOException {
637             writeIndicator(":", false, false, false);
638             states.push(new ExpectBlockMappingKey(false));
639             expectNode(false, false, true, false);
640         }
641     }
642 
643     private class ExpectBlockMappingValue implements EmitterState {
644         public void expect() throws IOException {
645             writeIndent();
646             writeIndicator(":", true, false, true);
647             states.push(new ExpectBlockMappingKey(false));
648             expectNode(false, false, true, false);
649         }
650     }
651 
652     // Checkers.
653 
654     private boolean checkEmptySequence() {
655         return (event instanceof SequenceStartEvent && !events.isEmpty() && events.peek() instanceof SequenceEndEvent);
656     }
657 
658     private boolean checkEmptyMapping() {
659         return (event instanceof MappingStartEvent && !events.isEmpty() && events.peek() instanceof MappingEndEvent);
660     }
661 
662     private boolean checkEmptyDocument() {
663         if (!(event instanceof DocumentStartEvent) || events.isEmpty()) {
664             return false;
665         }
666         Event event = events.peek();
667         if (event instanceof ScalarEvent) {
668             ScalarEvent e = (ScalarEvent) event;
669             return (e.getAnchor() == null && e.getTag() == null && e.getImplicit() != null && e
670                     .getValue() == "");
671         }
672         return false;
673     }
674 
675     private boolean checkSimpleKey() {
676         int length = 0;
677         if (event instanceof NodeEvent && ((NodeEvent) event).getAnchor() != null) {
678             if (preparedAnchor == null) {
679                 preparedAnchor = prepareAnchor(((NodeEvent) event).getAnchor());
680             }
681             length += preparedAnchor.length();
682         }
683         String tag = null;
684         if (event instanceof ScalarEvent) {
685             tag = ((ScalarEvent) event).getTag();
686         } else if (event instanceof CollectionStartEvent) {
687             tag = ((CollectionStartEvent) event).getTag();
688         }
689         if (tag != null) {
690             if (preparedTag == null) {
691                 preparedTag = prepareTag(tag);
692             }
693             length += preparedTag.length();
694         }
695         if (event instanceof ScalarEvent) {
696             if (analysis == null) {
697                 analysis = analyzeScalar(((ScalarEvent) event).getValue());
698             }
699             length += analysis.scalar.length();
700         }
701         return (length < 128 && (event instanceof AliasEvent
702                 || (event instanceof ScalarEvent && !analysis.empty && !analysis.multiline)
703                 || checkEmptySequence() || checkEmptyMapping()));
704     }
705 
706     // Anchor, Tag, and Scalar processors.
707 
708     private void processAnchor(String indicator) throws IOException {
709         NodeEvent ev = (NodeEvent) event;
710         if (ev.getAnchor() == null) {
711             preparedAnchor = null;
712             return;
713         }
714         if (preparedAnchor == null) {
715             preparedAnchor = prepareAnchor(ev.getAnchor());
716         }
717         writeIndicator(indicator + preparedAnchor, true, false, false);
718         preparedAnchor = null;
719     }
720 
721     private void processTag() throws IOException {
722         String tag = null;
723         if (event instanceof ScalarEvent) {
724             ScalarEvent ev = (ScalarEvent) event;
725             tag = ev.getTag();
726             if (style == null) {
727                 style = chooseScalarStyle();
728             }
729             if (((!canonical || tag == null) && ((style == null && ev.getImplicit()
730                     .canOmitTagInPlainScalar()) || (style != null && ev.getImplicit()
731                     .canOmitTagInNonPlainScalar())))) {
732                 preparedTag = null;
733                 return;
734             }
735             if (ev.getImplicit().canOmitTagInPlainScalar() && tag == null) {
736                 tag = "!";
737                 preparedTag = null;
738             }
739         } else {
740             CollectionStartEvent ev = (CollectionStartEvent) event;
741             tag = ev.getTag();
742             if ((!canonical || tag == null) && ev.getImplicit()) {
743                 preparedTag = null;
744                 return;
745             }
746         }
747         if (tag == null) {
748             throw new EmitterException("tag is not specified");
749         }
750         if (preparedTag == null) {
751             preparedTag = prepareTag(tag);
752         }
753         writeIndicator(preparedTag, true, false, false);
754         preparedTag = null;
755     }
756 
757     private Character chooseScalarStyle() {
758         ScalarEvent ev = (ScalarEvent) event;
759         if (analysis == null) {
760             analysis = analyzeScalar(ev.getValue());
761         }
762         if (ev.getStyle() != null && ev.getStyle() == '"' || this.canonical) {
763             return '"';
764         }
765         if (ev.getStyle() == null && ev.getImplicit().canOmitTagInPlainScalar()) {
766             if (!(simpleKeyContext && (analysis.empty || analysis.multiline))
767                     && ((flowLevel != 0 && analysis.allowFlowPlain) || (flowLevel == 0 && analysis.allowBlockPlain))) {
768                 return null;
769             }
770         }
771         if (ev.getStyle() != null && (ev.getStyle() == '|' || ev.getStyle() == '>')) {
772             if (flowLevel == 0 && !simpleKeyContext && analysis.allowBlock) {
773                 return ev.getStyle();
774             }
775         }
776         if (ev.getStyle() == null || ev.getStyle() == '\'') {
777             if (analysis.allowSingleQuoted && !(simpleKeyContext && analysis.multiline)) {
778                 return '\'';
779             }
780         }
781         return '"';
782     }
783 
784     private void processScalar() throws IOException {
785         ScalarEvent ev = (ScalarEvent) event;
786         if (analysis == null) {
787             analysis = analyzeScalar(ev.getValue());
788         }
789         if (style == null) {
790             style = chooseScalarStyle();
791         }
792         style = options.calculateScalarStyle(analysis, ScalarStyle.createStyle(style)).getChar();
793         boolean split = !simpleKeyContext;
794         if (style == null) {
795             writePlain(analysis.scalar, split);
796         } else {
797             switch (style) {
798             case '"':
799                 writeDoubleQuoted(analysis.scalar, split);
800                 break;
801             case '\'':
802                 writeSingleQuoted(analysis.scalar, split);
803                 break;
804             case '>':
805                 writeFolded(analysis.scalar);
806                 break;
807             case '|':
808                 writeLiteral(analysis.scalar);
809                 break;
810             }
811         }
812         analysis = null;
813         style = null;
814     }
815 
816     // Analyzers.
817 
818     private String prepareVersion(Integer[] version) {
819         Integer major = version[0];
820         Integer minor = version[1];
821         if (major != 1) {
822             throw new EmitterException("unsupported YAML version: " + version[0] + "." + version[1]);
823         }
824         return major.toString() + "." + minor.toString();
825     }
826 
827     private final static Pattern HANDLE_FORMAT = Pattern.compile("^![-_\\w]*!$");
828 
829     private String prepareTagHandle(String handle) {
830         if (handle.length() == 0) {
831             throw new EmitterException("tag handle must not be empty");
832         } else if (handle.charAt(0) != '!' || handle.charAt(handle.length() - 1) != '!') {
833             throw new EmitterException("tag handle must start and end with '!': " + handle);
834         } else if (!"!".equals(handle) && !HANDLE_FORMAT.matcher(handle).matches()) {
835             throw new EmitterException("invalid character in the tag handle: " + handle);
836         }
837         return handle;
838     }
839 
840     private String prepareTagPrefix(String prefix) {
841         if (prefix.length() == 0) {
842             throw new EmitterException("tag prefix must not be empty");
843         }
844         StringBuilder chunks = new StringBuilder();
845         int start = 0;
846         int end = 0;
847         if (prefix.charAt(0) == '!') {
848             end = 1;
849         }
850         while (end < prefix.length()) {
851             end++;
852         }
853         if (start < end) {
854             chunks.append(prefix.substring(start, end));
855         }
856         return chunks.toString();
857     }
858 
859     private String prepareTag(String tag) {
860         if (tag.length() == 0) {
861             throw new EmitterException("tag must not be empty");
862         }
863         if ("!".equals(tag)) {
864             return tag;
865         }
866         String handle = null;
867         String suffix = tag;
868         for (String prefix : tagPrefixes.keySet()) {
869             if (tag.startsWith(prefix) && ("!".equals(prefix) || prefix.length() < tag.length())) {
870                 handle = prefix;
871             }
872         }
873         if (handle != null) {
874             suffix = tag.substring(handle.length());
875             handle = tagPrefixes.get(handle);
876         }
877 
878         int end = suffix.length();
879         String suffixText = end > 0 ? suffix.substring(0, end) : "";
880 
881         if (handle != null) {
882             return handle + suffixText;
883         }
884         return "!<" + suffixText + ">";
885     }
886 
887     private final static Pattern ANCHOR_FORMAT = Pattern.compile("^[-_\\w]*$");
888 
889     static String prepareAnchor(String anchor) {
890         if (anchor.length() == 0) {
891             throw new EmitterException("anchor must not be empty");
892         }
893         if (!ANCHOR_FORMAT.matcher(anchor).matches()) {
894             throw new EmitterException("invalid character in the anchor: " + anchor);
895         }
896         return anchor;
897     }
898 
899     private ScalarAnalysis analyzeScalar(String scalar) {
900         // Empty scalar is a special case.
901         if (scalar.length() == 0) {
902             return new ScalarAnalysis(scalar, true, false, false, true, true, true, false);
903         }
904         // Indicators and special characters.
905         boolean blockIndicators = false;
906         boolean flowIndicators = false;
907         boolean lineBreaks = false;
908         boolean specialCharacters = false;
909 
910         // Important whitespace combinations.
911         boolean leadingSpace = false;
912         boolean leadingBreak = false;
913         boolean trailingSpace = false;
914         boolean trailingBreak = false;
915         boolean breakSpace = false;
916         boolean spaceBreak = false;
917 
918         // Check document indicators.
919         if (scalar.startsWith("---") || scalar.startsWith("...")) {
920             blockIndicators = true;
921             flowIndicators = true;
922         }
923         // First character or preceded by a whitespace.
924         boolean preceededByWhitespace = true;
925         boolean followedByWhitespace = (scalar.length() == 1 || Constant.NULL_BL_T_LINEBR
926                 .has(scalar.charAt(1)));
927         // The previous character is a space.
928         boolean previousSpace = false;
929 
930         // The previous character is a break.
931         boolean previousBreak = false;
932 
933         int index = 0;
934 
935         while (index < scalar.length()) {
936             char ch = scalar.charAt(index);
937             // Check for indicators.
938             if (index == 0) {
939                 // Leading indicators are special characters.
940                 if ("#,[]{}&*!|>\'\"%@`".indexOf(ch) != -1) {
941                     flowIndicators = true;
942                     blockIndicators = true;
943                 }
944                 if (ch == '?' || ch == ':') {
945                     flowIndicators = true;
946                     if (followedByWhitespace) {
947                         blockIndicators = true;
948                     }
949                 }
950                 if (ch == '-' && followedByWhitespace) {
951                     flowIndicators = true;
952                     blockIndicators = true;
953                 }
954             } else {
955                 // Some indicators cannot appear within a scalar as well.
956                 if (",?[]{}".indexOf(ch) != -1) {
957                     flowIndicators = true;
958                 }
959                 if (ch == ':') {
960                     flowIndicators = true;
961                     if (followedByWhitespace) {
962                         blockIndicators = true;
963                     }
964                 }
965                 if (ch == '#' && preceededByWhitespace) {
966                     flowIndicators = true;
967                     blockIndicators = true;
968                 }
969             }
970             // Check for line breaks, special, and unicode characters.
971             boolean isLineBreak = Constant.LINEBR.has(ch);
972             if (isLineBreak) {
973                 lineBreaks = true;
974             }
975             if (!(ch == '\n' || ('\u0020' <= ch && ch <= '\u007E'))) {
976                 if ((ch == '\u0085' || ('\u00A0' <= ch && ch <= '\uD7FF') || ('\uE000' <= ch && ch <= '\uFFFD'))
977                         && (ch != '\uFEFF')) {
978                     // unicode is used
979                     if (!this.allowUnicode) {
980                         specialCharacters = true;
981                     }
982                 } else {
983                     specialCharacters = true;
984                 }
985             }
986             // Detect important whitespace combinations.
987             if (ch == ' ') {
988                 if (index == 0) {
989                     leadingSpace = true;
990                 }
991                 if (index == scalar.length() - 1) {
992                     trailingSpace = true;
993                 }
994                 if (previousBreak) {
995                     breakSpace = true;
996                 }
997                 previousSpace = true;
998                 previousBreak = false;
999             } else if (isLineBreak) {
1000                 if (index == 0) {
1001                     leadingBreak = true;
1002                 }
1003                 if (index == scalar.length() - 1) {
1004                     trailingBreak = true;
1005                 }
1006                 if (previousSpace) {
1007                     spaceBreak = true;
1008                 }
1009                 previousSpace = false;
1010                 previousBreak = true;
1011             } else {
1012                 previousSpace = false;
1013                 previousBreak = false;
1014             }
1015 
1016             // Prepare for the next character.
1017             index++;
1018             preceededByWhitespace = Constant.NULL_BL_T.has(ch) || isLineBreak;
1019             followedByWhitespace = (index + 1 >= scalar.length()
1020                     || (Constant.NULL_BL_T.has(scalar.charAt(index + 1))) || isLineBreak);
1021         }
1022         // Let's decide what styles are allowed.
1023         boolean allowFlowPlain = true;
1024         boolean allowBlockPlain = true;
1025         boolean allowSingleQuoted = true;
1026         boolean allowDoubleQuoted = true;
1027         boolean allowBlock = true;
1028         // Leading and trailing whitespaces are bad for plain scalars.
1029         if (leadingSpace || leadingBreak || trailingSpace || trailingBreak) {
1030             allowFlowPlain = allowBlockPlain = false;
1031         }
1032         // We do not permit trailing spaces for block scalars.
1033         if (trailingSpace) {
1034             allowBlock = false;
1035         }
1036         // Spaces at the beginning of a new line are only acceptable for block
1037         // scalars.
1038         if (breakSpace) {
1039             allowFlowPlain = allowBlockPlain = allowSingleQuoted = false;
1040         }
1041         // Spaces followed by breaks, as well as special character are only
1042         // allowed for double quoted scalars.
1043         if (spaceBreak || specialCharacters) {
1044             allowFlowPlain = allowBlockPlain = allowSingleQuoted = allowBlock = false;
1045         }
1046         // Although the plain scalar writer supports breaks, we never emit
1047         // multiline plain scalars.
1048         if (lineBreaks) {
1049             allowFlowPlain = allowBlockPlain = false;
1050         }
1051         // Flow indicators are forbidden for flow plain scalars.
1052         if (flowIndicators) {
1053             allowFlowPlain = false;
1054         }
1055         // Block indicators are forbidden for block plain scalars.
1056         if (blockIndicators) {
1057             allowBlockPlain = false;
1058         }
1059 
1060         return new ScalarAnalysis(scalar, false, lineBreaks, allowFlowPlain, allowBlockPlain,
1061                 allowSingleQuoted, allowDoubleQuoted, allowBlock);
1062     }
1063 
1064     // Writers.
1065 
1066     void flushStream() throws IOException {
1067         stream.flush();
1068     }
1069 
1070     void writeStreamStart() {
1071         // BOM is written by Writer.
1072     }
1073 
1074     void writeStreamEnd() throws IOException {
1075         flushStream();
1076     }
1077 
1078     void writeIndicator(String indicator, boolean needWhitespace, boolean whitespace,
1079             boolean indentation) throws IOException {
1080         if (!this.whitespace && needWhitespace) {
1081             this.column++;
1082             stream.write(SPACE);
1083         }
1084         this.whitespace = whitespace;
1085         this.indention = this.indention && indentation;
1086         this.column += indicator.length();
1087         openEnded = false;
1088         stream.write(indicator);
1089     }
1090 
1091     void writeIndent() throws IOException {
1092         int indent;
1093         if (this.indent != null) {
1094             indent = this.indent;
1095         } else {
1096             indent = 0;
1097         }
1098 
1099         if (!this.indention || this.column > indent || (this.column == indent && !this.whitespace)) {
1100             writeLineBreak(null);
1101         }
1102 
1103         if (this.column < indent) {
1104             this.whitespace = true;
1105             char[] data = new char[indent - this.column];
1106             for (int i = 0; i < data.length; i++) {
1107                 data[i] = ' ';
1108             }
1109             this.column = indent;
1110             stream.write(data);
1111         }
1112     }
1113 
1114     private void writeLineBreak(String data) throws IOException {
1115         this.whitespace = true;
1116         this.indention = true;
1117         this.column = 0;
1118         if (data == null) {
1119             stream.write(this.bestLineBreak);
1120         } else {
1121             stream.write(data);
1122         }
1123     }
1124 
1125     void writeVersionDirective(String versionText) throws IOException {
1126         stream.write("%YAML ");
1127         stream.write(versionText);
1128         writeLineBreak(null);
1129     }
1130 
1131     void writeTagDirective(String handleText, String prefixText) throws IOException {
1132         // XXX: not sure 4 invocations better then StringBuilders created by str
1133         // + str
1134         stream.write("%TAG ");
1135         stream.write(handleText);
1136         stream.write(SPACE);
1137         stream.write(prefixText);
1138         writeLineBreak(null);
1139     }
1140 
1141     // Scalar streams.
1142     private void writeSingleQuoted(String text, boolean split) throws IOException {
1143         writeIndicator("'", true, false, false);
1144         boolean spaces = false;
1145         boolean breaks = false;
1146         int start = 0, end = 0;
1147         char ch;
1148         while (end <= text.length()) {
1149             ch = 0;
1150             if (end < text.length()) {
1151                 ch = text.charAt(end);
1152             }
1153             if (spaces) {
1154                 if (ch == 0 || ch != ' ') {
1155                     if (start + 1 == end && this.column > this.bestWidth && split && start != 0
1156                             && end != text.length()) {
1157                         writeIndent();
1158                     } else {
1159                         int len = end - start;
1160                         this.column += len;
1161                         stream.write(text, start, len);
1162                     }
1163                     start = end;
1164                 }
1165             } else if (breaks) {
1166                 if (ch == 0 || Constant.LINEBR.hasNo(ch)) {
1167                     if (text.charAt(start) == '\n') {
1168                         writeLineBreak(null);
1169                     }
1170                     String data = text.substring(start, end);
1171                     for (char br : data.toCharArray()) {
1172                         if (br == '\n') {
1173                             writeLineBreak(null);
1174                         } else {
1175                             writeLineBreak(String.valueOf(br));
1176                         }
1177                     }
1178                     writeIndent();
1179                     start = end;
1180                 }
1181             } else {
1182                 if (Constant.LINEBR.has(ch, "\0 \'")) {
1183                     if (start < end) {
1184                         int len = end - start;
1185                         this.column += len;
1186                         stream.write(text, start, len);
1187                         start = end;
1188                     }
1189                 }
1190             }
1191             if (ch == '\'') {
1192                 this.column += 2;
1193                 stream.write("''");
1194                 start = end + 1;
1195             }
1196             if (ch != 0) {
1197                 spaces = ch == ' ';
1198                 breaks = Constant.LINEBR.has(ch);
1199             }
1200             end++;
1201         }
1202         writeIndicator("'", false, false, false);
1203     }
1204 
1205     private void writeDoubleQuoted(String text, boolean split) throws IOException {
1206         writeIndicator("\"", true, false, false);
1207         int start = 0;
1208         int end = 0;
1209         while (end <= text.length()) {
1210             Character ch = null;
1211             if (end < text.length()) {
1212                 ch = text.charAt(end);
1213             }
1214             if (ch == null || "\"\\\u0085\u2028\u2029\uFEFF".indexOf(ch) != -1
1215                     || !('\u0020' <= ch && ch <= '\u007E')) {
1216                 if (start < end) {
1217                     int len = end - start;
1218                     this.column += len;
1219                     stream.write(text, start, len);
1220                     start = end;
1221                 }
1222                 if (ch != null) {
1223                     String data;
1224                     if (ESCAPE_REPLACEMENTS.containsKey(new Character(ch))) {
1225                         data = "\\" + ESCAPE_REPLACEMENTS.get(new Character(ch));
1226                     } else if (!this.allowUnicode) {
1227                         // this is different from PyYAML which escapes all
1228                         // non-ASCII characters
1229                         if (ch <= '\u00FF') {
1230                             String s = "0" + Integer.toString(ch, 16);
1231                             data = "\\x" + s.substring(s.length() - 2);
1232                         } else {
1233                             String s = "000" + Integer.toString(ch, 16);
1234                             data = "\\u" + s.substring(s.length() - 4);
1235                         }
1236                     } else {
1237                         data = String.valueOf(ch);
1238                     }
1239                     this.column += data.length();
1240                     stream.write(data);
1241                     start = end + 1;
1242                 }
1243             }
1244             if ((0 < end && end < (text.length() - 1)) && (ch == ' ' || start >= end)
1245                     && (this.column + (end - start)) > this.bestWidth && split) {
1246                 String data;
1247                 if (start >= end) {
1248                     data = "\\";
1249                 } else {
1250                     data = text.substring(start, end) + "\\";
1251                 }
1252                 if (start < end) {
1253                     start = end;
1254                 }
1255                 this.column += data.length();
1256                 stream.write(data);
1257                 writeIndent();
1258                 this.whitespace = false;
1259                 this.indention = false;
1260                 if (text.charAt(start) == ' ') {
1261                     data = "\\";
1262                     this.column += data.length();
1263                     stream.write(data);
1264                 }
1265             }
1266             end += 1;
1267         }
1268         writeIndicator("\"", false, false, false);
1269     }
1270 
1271     private String determineBlockHints(String text) {
1272         StringBuilder hints = new StringBuilder();
1273         if (Constant.LINEBR.has(text.charAt(0), " ")) {
1274             hints.append(bestIndent);
1275         }
1276         char ch1 = text.charAt(text.length() - 1);
1277         if (Constant.LINEBR.hasNo(ch1)) {
1278             hints.append("-");
1279         } else if (text.length() == 1 || Constant.LINEBR.has(text.charAt(text.length() - 2))) {
1280             hints.append("+");
1281         }
1282         return hints.toString();
1283     }
1284 
1285     void writeFolded(String text) throws IOException {
1286         String hints = determineBlockHints(text);
1287         writeIndicator(">" + hints, true, false, false);
1288         if (hints.length() > 0 && (hints.charAt(hints.length() - 1) == '+')) {
1289             openEnded = true;
1290         }
1291         writeLineBreak(null);
1292         boolean leadingSpace = true;
1293         boolean spaces = false;
1294         boolean breaks = true;
1295         int start = 0, end = 0;
1296         while (end <= text.length()) {
1297             char ch = 0;
1298             if (end < text.length()) {
1299                 ch = text.charAt(end);
1300             }
1301             if (breaks) {
1302                 if (ch == 0 || Constant.LINEBR.hasNo(ch)) {
1303                     if (!leadingSpace && ch != 0 && ch != ' ' && text.charAt(start) == '\n') {
1304                         writeLineBreak(null);
1305                     }
1306                     leadingSpace = (ch == ' ');
1307                     String data = text.substring(start, end);
1308                     for (char br : data.toCharArray()) {
1309                         if (br == '\n') {
1310                             writeLineBreak(null);
1311                         } else {
1312                             writeLineBreak(String.valueOf(br));
1313                         }
1314                     }
1315                     if (ch != 0) {
1316                         writeIndent();
1317                     }
1318                     start = end;
1319                 }
1320             } else if (spaces) {
1321                 if (ch != ' ') {
1322                     if (start + 1 == end && this.column > this.bestWidth) {
1323                         writeIndent();
1324                     } else {
1325                         int len = end - start;
1326                         this.column += len;
1327                         stream.write(text, start, len);
1328                     }
1329                     start = end;
1330                 }
1331             } else {
1332                 if (Constant.LINEBR.has(ch, "\0 ")) {
1333                     int len = end - start;
1334                     this.column += len;
1335                     stream.write(text, start, len);
1336                     if (ch == 0) {
1337                         writeLineBreak(null);
1338                     }
1339                     start = end;
1340                 }
1341             }
1342             if (ch != 0) {
1343                 breaks = Constant.LINEBR.has(ch);
1344                 spaces = (ch == ' ');
1345             }
1346             end++;
1347         }
1348     }
1349 
1350     void writeLiteral(String text) throws IOException {
1351         String hints = determineBlockHints(text);
1352         writeIndicator("|" + hints, true, false, false);
1353         if (hints.length() > 0 && (hints.charAt(hints.length() - 1)) == '+') {
1354             openEnded = true;
1355         }
1356         writeLineBreak(null);
1357         boolean breaks = true;
1358         int start = 0, end = 0;
1359         while (end <= text.length()) {
1360             char ch = 0;
1361             if (end < text.length()) {
1362                 ch = text.charAt(end);
1363             }
1364             if (breaks) {
1365                 if (ch == 0 || Constant.LINEBR.hasNo(ch)) {
1366                     String data = text.substring(start, end);
1367                     for (char br : data.toCharArray()) {
1368                         if (br == '\n') {
1369                             writeLineBreak(null);
1370                         } else {
1371                             writeLineBreak(String.valueOf(br));
1372                         }
1373                     }
1374                     if (ch != 0) {
1375                         writeIndent();
1376                     }
1377                     start = end;
1378                 }
1379             } else {
1380                 if (ch == 0 || Constant.LINEBR.has(ch)) {
1381                     stream.write(text, start, end - start);
1382                     if (ch == 0) {
1383                         writeLineBreak(null);
1384                     }
1385                     start = end;
1386                 }
1387             }
1388             if (ch != 0) {
1389                 breaks = (Constant.LINEBR.has(ch));
1390             }
1391             end++;
1392         }
1393     }
1394 
1395     void writePlain(String text, boolean split) throws IOException {
1396         if (rootContext) {
1397             openEnded = true;
1398         }
1399         if (text.length() == 0) {
1400             return;
1401         }
1402         if (!this.whitespace) {
1403             this.column++;
1404             stream.write(SPACE);
1405         }
1406         this.whitespace = false;
1407         this.indention = false;
1408         boolean spaces = false;
1409         boolean breaks = false;
1410         int start = 0, end = 0;
1411         while (end <= text.length()) {
1412             char ch = 0;
1413             if (end < text.length()) {
1414                 ch = text.charAt(end);
1415             }
1416             if (spaces) {
1417                 if (ch != ' ') {
1418                     if (start + 1 == end && this.column > this.bestWidth && split) {
1419                         writeIndent();
1420                         this.whitespace = false;
1421                         this.indention = false;
1422                     } else {
1423                         int len = end - start;
1424                         this.column += len;
1425                         stream.write(text, start, len);
1426                     }
1427                     start = end;
1428                 }
1429             } else if (breaks) {
1430                 if (Constant.LINEBR.hasNo(ch)) {
1431                     if (text.charAt(start) == '\n') {
1432                         writeLineBreak(null);
1433                     }
1434                     String data = text.substring(start, end);
1435                     for (char br : data.toCharArray()) {
1436                         if (br == '\n') {
1437                             writeLineBreak(null);
1438                         } else {
1439                             writeLineBreak(String.valueOf(br));
1440                         }
1441                     }
1442                     writeIndent();
1443                     this.whitespace = false;
1444                     this.indention = false;
1445                     start = end;
1446                 }
1447             } else {
1448                 if (ch == 0 || Constant.LINEBR.has(ch)) {
1449                     int len = end - start;
1450                     this.column += len;
1451                     stream.write(text, start, len);
1452                     start = end;
1453                 }
1454             }
1455             if (ch != 0) {
1456                 spaces = (ch == ' ');
1457                 breaks = (Constant.LINEBR.has(ch));
1458             }
1459             end++;
1460         }
1461     }
1462 }