View Javadoc

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