View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.doxia.module.apt;
20  
21  import javax.swing.text.MutableAttributeSet;
22  
23  import java.io.PrintWriter;
24  import java.io.Writer;
25  import java.util.ArrayList;
26  import java.util.Collection;
27  import java.util.LinkedList;
28  import java.util.List;
29  import java.util.Stack;
30  
31  import org.apache.commons.lang3.StringUtils;
32  import org.apache.maven.doxia.sink.SinkEventAttributes;
33  import org.apache.maven.doxia.sink.impl.AbstractTextSink;
34  import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet;
35  import org.apache.maven.doxia.sink.impl.SinkUtils;
36  import org.slf4j.Logger;
37  import org.slf4j.LoggerFactory;
38  
39  /**
40   * APT generator implementation.
41   * <br>
42   * <b>Note</b>: The encoding used is UTF-8.
43   *
44   * @author eredmond
45   * @since 1.0
46   */
47  public class AptSink extends AbstractTextSink implements AptMarkup {
48      private static final Logger LOGGER = LoggerFactory.getLogger(AptSink.class);
49  
50      // ----------------------------------------------------------------------
51      // Instance fields
52      // ----------------------------------------------------------------------
53  
54      /**  A buffer that holds the current text when headerFlag or bufferFlag set to <code>true</code>. */
55      private StringBuffer buffer;
56  
57      /**  A buffer that holds the table caption. */
58      private StringBuilder tableCaptionBuffer;
59  
60      /**  authors. */
61      private Collection<String> authors;
62  
63      /**  title. */
64      private String title;
65  
66      /**  date. */
67      private String date;
68  
69      /** startFlag. */
70      private boolean startFlag;
71  
72      /**  tableCaptionFlag. */
73      private boolean tableCaptionFlag;
74  
75      /**  tableCellFlag. */
76      private boolean tableCellFlag;
77  
78      /**  headerFlag. */
79      private boolean headerFlag;
80  
81      /**  bufferFlag. */
82      private boolean bufferFlag;
83  
84      /**  itemFlag. */
85      private boolean itemFlag;
86  
87      /**  verbatimFlag. */
88      private boolean verbatimFlag;
89  
90      /**  verbatim source. */
91      private boolean isSource;
92  
93      /**  gridFlag for tables. */
94      private boolean gridFlag;
95  
96      /**  number of cells in a table. */
97      private int cellCount;
98  
99      /**  The writer to use. */
100     private final PrintWriter writer;
101 
102     /**  justification of table cells. */
103     private int[] cellJustif;
104 
105     /**  a line of a row in a table. */
106     private String rowLine;
107 
108     /**  listNestingIndent. */
109     private String listNestingIndent;
110 
111     /**  listStyles. */
112     private final Stack<String> listStyles;
113 
114     /** Keep track of the closing tags for inline events. */
115     protected Stack<List<String>> inlineStack = new Stack<>();
116 
117     // ----------------------------------------------------------------------
118     // Public protected methods
119     // ----------------------------------------------------------------------
120 
121     /**
122      * Constructor, initialize the Writer and the variables.
123      *
124      * @param writer not null writer to write the result. <b>Should</b> be an UTF-8 Writer.
125      * You could use <code>newWriter</code> methods from {@code org.codehaus.plexus.util.WriterFactory}.
126      */
127     protected AptSink(Writer writer) {
128         this.writer = new PrintWriter(writer);
129         this.listStyles = new Stack<>();
130 
131         init();
132     }
133 
134     /**
135      * Returns the buffer that holds the current text.
136      *
137      * @return A StringBuffer.
138      */
139     protected StringBuffer getBuffer() {
140         return buffer;
141     }
142 
143     /**
144      * Used to determine whether we are in head mode.
145      *
146      * @param headFlag True for head mode.
147      */
148     protected void setHeadFlag(boolean headFlag) {
149         this.headerFlag = headFlag;
150     }
151 
152     /**
153      * {@inheritDoc}
154      */
155     protected void init() {
156         super.init();
157 
158         resetBuffer();
159 
160         this.tableCaptionBuffer = new StringBuilder();
161         this.listNestingIndent = "";
162 
163         this.authors = new LinkedList<>();
164         this.title = null;
165         this.date = null;
166         this.startFlag = true;
167         this.tableCaptionFlag = false;
168         this.tableCellFlag = false;
169         this.headerFlag = false;
170         this.bufferFlag = false;
171         this.itemFlag = false;
172         this.verbatimFlag = false;
173         this.isSource = false;
174         this.gridFlag = false;
175         this.cellCount = 0;
176         this.cellJustif = null;
177         this.rowLine = null;
178         this.listStyles.clear();
179         this.inlineStack.clear();
180     }
181 
182     /**
183      * Reset the StringBuilder.
184      */
185     protected void resetBuffer() {
186         buffer = new StringBuffer();
187     }
188 
189     /**
190      * Reset the TableCaptionBuffer.
191      */
192     protected void resetTableCaptionBuffer() {
193         tableCaptionBuffer = new StringBuilder();
194     }
195 
196     @Override
197     public void head(SinkEventAttributes attributes) {
198         boolean startFlag = this.startFlag;
199 
200         init();
201 
202         headerFlag = true;
203         this.startFlag = startFlag;
204     }
205 
206     /**
207      * {@inheritDoc}
208      */
209     public void head_() {
210         headerFlag = false;
211 
212         if (!startFlag) {
213             write(EOL);
214         }
215         write(HEADER_START_MARKUP + EOL);
216         if (title != null) {
217             write(" " + title + EOL);
218         }
219         write(HEADER_START_MARKUP + EOL);
220         for (String author : authors) {
221             write(" " + author + EOL);
222         }
223         write(HEADER_START_MARKUP + EOL);
224         if (date != null) {
225             write(" " + date + EOL);
226         }
227         write(HEADER_START_MARKUP + EOL);
228     }
229 
230     /**
231      * {@inheritDoc}
232      */
233     public void title_() {
234         if (buffer.length() > 0) {
235             title = buffer.toString();
236             resetBuffer();
237         }
238     }
239 
240     /**
241      * {@inheritDoc}
242      */
243     public void author_() {
244         if (buffer.length() > 0) {
245             authors.add(buffer.toString());
246             resetBuffer();
247         }
248     }
249 
250     /**
251      * {@inheritDoc}
252      */
253     public void date_() {
254         if (buffer.length() > 0) {
255             date = buffer.toString();
256             resetBuffer();
257         }
258     }
259 
260     @Override
261     public void section_(int level) {
262         write(EOL);
263     }
264 
265     @Override
266     public void sectionTitle(int level, SinkEventAttributes attributes) {
267         if (level > 5) {
268             LOGGER.warn(
269                     "{}Replacing unsupported section title level {} in APT with level 5",
270                     getLocationLogPrefix(),
271                     level);
272             level = 5;
273         }
274         if (level == 1) {
275             write(EOL);
276         } else if (level > 1) {
277             write(EOL + StringUtils.repeat(SECTION_TITLE_START_MARKUP, level - 1));
278         }
279     }
280 
281     @Override
282     public void sectionTitle_(int level) {
283         if (level >= 1) {
284             write(EOL + EOL);
285         }
286     }
287 
288     @Override
289     public void list(SinkEventAttributes attributes) {
290         listNestingIndent += " ";
291         listStyles.push(LIST_START_MARKUP);
292         write(EOL);
293     }
294 
295     /**
296      * {@inheritDoc}
297      */
298     public void list_() {
299         if (listNestingIndent.length() <= 1) {
300             write(EOL + listNestingIndent + LIST_END_MARKUP + EOL);
301         } else {
302             write(EOL);
303         }
304         listNestingIndent = StringUtils.chomp(listNestingIndent, " ");
305         listStyles.pop();
306         itemFlag = false;
307     }
308 
309     @Override
310     public void listItem(SinkEventAttributes attributes) {
311         // if (!numberedList)
312         // write(EOL + listNestingIndent + "*");
313         // else
314         numberedListItem();
315         itemFlag = true;
316     }
317 
318     /**
319      * {@inheritDoc}
320      */
321     public void listItem_() {
322         write(EOL);
323         itemFlag = false;
324     }
325 
326     @Override
327     public void numberedList(int numbering, SinkEventAttributes attributes) {
328         listNestingIndent += " ";
329         write(EOL);
330 
331         String style;
332         switch (numbering) {
333             case NUMBERING_UPPER_ALPHA:
334                 style = String.valueOf(NUMBERING_UPPER_ALPHA_CHAR);
335                 break;
336             case NUMBERING_LOWER_ALPHA:
337                 style = String.valueOf(NUMBERING_LOWER_ALPHA_CHAR);
338                 break;
339             case NUMBERING_UPPER_ROMAN:
340                 style = String.valueOf(NUMBERING_UPPER_ROMAN_CHAR);
341                 break;
342             case NUMBERING_LOWER_ROMAN:
343                 style = String.valueOf(NUMBERING_LOWER_ROMAN_CHAR);
344                 break;
345             case NUMBERING_DECIMAL:
346             default:
347                 style = String.valueOf(NUMBERING);
348         }
349 
350         listStyles.push(style);
351     }
352 
353     /**
354      * {@inheritDoc}
355      */
356     public void numberedList_() {
357         if (listNestingIndent.length() <= 1) {
358             write(EOL + listNestingIndent + LIST_END_MARKUP + EOL);
359         } else {
360             write(EOL);
361         }
362         listNestingIndent = StringUtils.chomp(listNestingIndent, " ");
363         listStyles.pop();
364         itemFlag = false;
365     }
366 
367     @Override
368     public void numberedListItem(SinkEventAttributes attributes) {
369         String style = listStyles.peek();
370         if (style.equals(String.valueOf(STAR))) {
371             write(EOL + listNestingIndent + STAR + SPACE);
372         } else {
373             write(EOL
374                     + listNestingIndent
375                     + LEFT_SQUARE_BRACKET
376                     + LEFT_SQUARE_BRACKET
377                     + style
378                     + RIGHT_SQUARE_BRACKET
379                     + RIGHT_SQUARE_BRACKET
380                     + SPACE);
381         }
382         itemFlag = true;
383     }
384 
385     /**
386      * {@inheritDoc}
387      */
388     public void numberedListItem_() {
389         write(EOL);
390         itemFlag = false;
391     }
392 
393     @Override
394     public void definitionList(SinkEventAttributes attributes) {
395         listNestingIndent += " ";
396         listStyles.push("");
397         write(EOL);
398     }
399 
400     /**
401      * {@inheritDoc}
402      */
403     public void definitionList_() {
404         if (listNestingIndent.length() <= 1) {
405             write(EOL + listNestingIndent + LIST_END_MARKUP + EOL);
406         } else {
407             write(EOL);
408         }
409         listNestingIndent = StringUtils.chomp(listNestingIndent, " ");
410         listStyles.pop();
411         itemFlag = false;
412     }
413 
414     @Override
415     public void definedTerm(SinkEventAttributes attributes) {
416         write(EOL + " [");
417     }
418 
419     /**
420      * {@inheritDoc}
421      */
422     public void definedTerm_() {
423         write("] ");
424     }
425 
426     @Override
427     public void definition(SinkEventAttributes attributes) {
428         itemFlag = true;
429     }
430 
431     /**
432      * {@inheritDoc}
433      */
434     public void definition_() {
435         write(EOL);
436         itemFlag = false;
437     }
438 
439     /**
440      * {@inheritDoc}
441      */
442     public void pageBreak() {
443         write(EOL + PAGE_BREAK + EOL);
444     }
445 
446     @Override
447     public void paragraph(SinkEventAttributes attributes) {
448         if (tableCellFlag) {
449             // ignore paragraphs in table cells
450         } else if (itemFlag) {
451             write(EOL + EOL + "  " + listNestingIndent);
452         } else {
453             write(EOL + " ");
454         }
455     }
456 
457     /**
458      * {@inheritDoc}
459      */
460     public void paragraph_() {
461         if (tableCellFlag) {
462             // ignore paragraphs in table cells
463         } else {
464             write(EOL + EOL);
465         }
466     }
467 
468     @Override
469     public void verbatim(SinkEventAttributes attributes) {
470         MutableAttributeSet atts = SinkUtils.filterAttributes(attributes, SinkUtils.SINK_VERBATIM_ATTRIBUTES);
471 
472         boolean source = false;
473 
474         if (atts != null && atts.isDefined(SinkEventAttributes.DECORATION)) {
475             source = "source"
476                     .equals(atts.getAttribute(SinkEventAttributes.DECORATION).toString());
477         }
478 
479         verbatimFlag = true;
480         this.isSource = source;
481         write(EOL);
482         if (source) {
483             write(EOL + VERBATIM_SOURCE_START_MARKUP + EOL);
484         } else {
485             write(EOL + VERBATIM_START_MARKUP + EOL);
486         }
487     }
488 
489     /**
490      * {@inheritDoc}
491      */
492     public void verbatim_() {
493         if (isSource) {
494             write(EOL + VERBATIM_SOURCE_END_MARKUP + EOL);
495         } else {
496             write(EOL + VERBATIM_END_MARKUP + EOL);
497         }
498         isSource = false;
499         verbatimFlag = false;
500     }
501 
502     @Override
503     public void horizontalRule(SinkEventAttributes attributes) {
504         write(EOL + HORIZONTAL_RULE_MARKUP + EOL);
505     }
506 
507     @Override
508     public void table(SinkEventAttributes attributes) {
509         write(EOL);
510     }
511 
512     /**
513      * {@inheritDoc}
514      */
515     public void table_() {
516         if (rowLine != null) {
517             write(rowLine);
518         }
519         rowLine = null;
520 
521         if (tableCaptionBuffer.length() > 0) {
522             text(tableCaptionBuffer.toString() + EOL);
523         }
524 
525         resetTableCaptionBuffer();
526     }
527 
528     @Override
529     public void tableRows(int[] justification, boolean grid) {
530         cellJustif = justification;
531         gridFlag = grid;
532     }
533 
534     /**
535      * {@inheritDoc}
536      */
537     public void tableRows_() {
538         cellJustif = null;
539         gridFlag = false;
540     }
541 
542     @Override
543     public void tableRow(SinkEventAttributes attributes) {
544         bufferFlag = true;
545         cellCount = 0;
546     }
547 
548     /**
549      * {@inheritDoc}
550      */
551     public void tableRow_() {
552         bufferFlag = false;
553 
554         // write out the header row first, then the data in the buffer
555         buildRowLine();
556 
557         write(rowLine);
558 
559         // TODO: This will need to be more clever, for multi-line cells
560         if (gridFlag) {
561             write(TABLE_ROW_SEPARATOR_MARKUP);
562         }
563 
564         write(buffer.toString());
565 
566         resetBuffer();
567 
568         write(EOL);
569 
570         // only reset cell count if this is the last row
571         cellCount = 0;
572     }
573 
574     /** Construct a table row. */
575     private void buildRowLine() {
576         StringBuilder rLine = new StringBuilder();
577         rLine.append(TABLE_ROW_START_MARKUP);
578 
579         for (int i = 0; i < cellCount; i++) {
580             if (cellJustif != null) {
581                 switch (cellJustif[i]) {
582                     case 1:
583                         rLine.append(TABLE_COL_LEFT_ALIGNED_MARKUP);
584                         break;
585                     case 2:
586                         rLine.append(TABLE_COL_RIGHT_ALIGNED_MARKUP);
587                         break;
588                     default:
589                         rLine.append(TABLE_COL_CENTERED_ALIGNED_MARKUP);
590                 }
591             } else {
592                 rLine.append(TABLE_COL_CENTERED_ALIGNED_MARKUP);
593             }
594         }
595         rLine.append(EOL);
596 
597         this.rowLine = rLine.toString();
598     }
599 
600     @Override
601     public void tableCell(SinkEventAttributes attributes) {
602         tableCell(false);
603     }
604 
605     @Override
606     public void tableHeaderCell(SinkEventAttributes attributes) {
607         tableCell(true);
608     }
609 
610     /**
611      * Starts a table cell.
612      *
613      * @param headerRow If this cell is part of a header row.
614      */
615     public void tableCell(boolean headerRow) {
616         if (headerRow) {
617             buffer.append(TABLE_CELL_SEPARATOR_MARKUP);
618         }
619         tableCellFlag = true;
620     }
621 
622     /**
623      * {@inheritDoc}
624      */
625     public void tableCell_() {
626         endTableCell();
627     }
628 
629     /**
630      * {@inheritDoc}
631      */
632     public void tableHeaderCell_() {
633         endTableCell();
634     }
635 
636     /**
637      * Ends a table cell.
638      */
639     private void endTableCell() {
640         tableCellFlag = false;
641         buffer.append(TABLE_CELL_SEPARATOR_MARKUP);
642         cellCount++;
643     }
644 
645     @Override
646     public void tableCaption(SinkEventAttributes attributes) {
647         tableCaptionFlag = true;
648     }
649 
650     /**
651      * {@inheritDoc}
652      */
653     public void tableCaption_() {
654         tableCaptionFlag = false;
655     }
656 
657     /**
658      * {@inheritDoc}
659      */
660     public void figureCaption_() {
661         write(EOL);
662     }
663 
664     @Override
665     public void figureGraphics(String name, SinkEventAttributes attributes) {
666         write(EOL + "[" + name + "] ");
667     }
668 
669     @Override
670     public void anchor(String name, SinkEventAttributes attributes) {
671         write(ANCHOR_START_MARKUP);
672     }
673 
674     /**
675      * {@inheritDoc}
676      */
677     public void anchor_() {
678         write(ANCHOR_END_MARKUP);
679     }
680 
681     @Override
682     public void link(String name, SinkEventAttributes attributes) {
683         if (!headerFlag) {
684             write(LINK_START_1_MARKUP);
685             text(name.startsWith("#") ? name.substring(1) : name);
686             write(LINK_START_2_MARKUP);
687         }
688     }
689 
690     /**
691      * {@inheritDoc}
692      */
693     public void link_() {
694         if (!headerFlag) {
695             write(LINK_END_MARKUP);
696         }
697     }
698 
699     /**
700      * A link with a target.
701      *
702      * @param name The name of the link.
703      * @param target The link target.
704      */
705     public void link(String name, String target) {
706         if (!headerFlag) {
707             write(LINK_START_1_MARKUP);
708             text(target);
709             write(LINK_START_2_MARKUP);
710             text(name);
711         }
712     }
713 
714     /** {@inheritDoc} */
715     public void inline(SinkEventAttributes attributes) {
716         if (!headerFlag) {
717             List<String> tags = new ArrayList<>();
718 
719             if (attributes != null) {
720 
721                 if (attributes.containsAttribute(SinkEventAttributes.SEMANTICS, "italic")) {
722                     write(ITALIC_START_MARKUP);
723                     tags.add(0, ITALIC_END_MARKUP);
724                 }
725 
726                 if (attributes.containsAttribute(SinkEventAttributes.SEMANTICS, "bold")) {
727                     write(BOLD_START_MARKUP);
728                     tags.add(0, BOLD_END_MARKUP);
729                 }
730 
731                 if (attributes.containsAttribute(SinkEventAttributes.SEMANTICS, "code")) {
732                     write(MONOSPACED_START_MARKUP);
733                     tags.add(0, MONOSPACED_END_MARKUP);
734                 }
735             }
736 
737             inlineStack.push(tags);
738         }
739     }
740 
741     /**
742      * {@inheritDoc}
743      */
744     public void inline_() {
745         if (!headerFlag) {
746             for (String tag : inlineStack.pop()) {
747                 write(tag);
748             }
749         }
750     }
751 
752     /**
753      * {@inheritDoc}
754      */
755     public void italic() {
756         inline(SinkEventAttributeSet.Semantics.ITALIC);
757     }
758 
759     /**
760      * {@inheritDoc}
761      */
762     public void italic_() {
763         inline_();
764     }
765 
766     /**
767      * {@inheritDoc}
768      */
769     public void bold() {
770         inline(SinkEventAttributeSet.Semantics.BOLD);
771     }
772 
773     /**
774      * {@inheritDoc}
775      */
776     public void bold_() {
777         inline_();
778     }
779 
780     /**
781      * {@inheritDoc}
782      */
783     public void monospaced() {
784         inline(SinkEventAttributeSet.Semantics.CODE);
785     }
786 
787     /**
788      * {@inheritDoc}
789      */
790     public void monospaced_() {
791         inline_();
792     }
793 
794     @Override
795     public void lineBreak(SinkEventAttributes attributes) {
796         if (headerFlag || bufferFlag) {
797             buffer.append(EOL);
798         } else if (verbatimFlag) {
799             write(EOL);
800         } else {
801             write("\\" + EOL);
802         }
803     }
804 
805     /**
806      * {@inheritDoc}
807      */
808     public void nonBreakingSpace() {
809         if (headerFlag || bufferFlag) {
810             buffer.append(NON_BREAKING_SPACE_MARKUP);
811         } else {
812             write(NON_BREAKING_SPACE_MARKUP);
813         }
814     }
815 
816     @Override
817     public void text(String text, SinkEventAttributes attributes) {
818         if (attributes != null) {
819             inline(attributes);
820         }
821         if (tableCaptionFlag) {
822             tableCaptionBuffer.append(text);
823         } else if (headerFlag || bufferFlag) {
824             buffer.append(text);
825         } else if (verbatimFlag) {
826             verbatimContent(text);
827         } else {
828             content(text);
829         }
830         if (attributes != null) {
831             inline_();
832         }
833     }
834 
835     /** {@inheritDoc} */
836     public void rawText(String text) {
837         write(text);
838     }
839 
840     /** {@inheritDoc} */
841     public void comment(String comment) {
842         rawText((startFlag ? "" : EOL) + COMMENT + COMMENT + comment);
843     }
844 
845     /**
846      * {@inheritDoc}
847      *
848      * Unkown events just log a warning message but are ignored otherwise.
849      * @see org.apache.maven.doxia.sink.Sink#unknown(String,Object[],SinkEventAttributes)
850      */
851     public void unknown(String name, Object[] requiredParams, SinkEventAttributes attributes) {
852         LOGGER.warn("{}Unknown Sink event '{}', ignoring!", getLocationLogPrefix(), name);
853     }
854 
855     /**
856      * Write text to output.
857      *
858      * @param text The text to write.
859      */
860     protected void write(String text) {
861         startFlag = false;
862         if (tableCellFlag) {
863             buffer.append(text);
864         } else {
865             writer.write(unifyEOLs(text));
866         }
867     }
868 
869     /**
870      * Write Apt escaped text to output.
871      *
872      * @param text The text to write.
873      */
874     protected void content(String text) {
875         write(escapeAPT(text));
876     }
877 
878     /**
879      * Write Apt escaped text to output.
880      *
881      * @param text The text to write.
882      */
883     protected void verbatimContent(String text) {
884         write(escapeAPT(text));
885     }
886 
887     /**
888      * {@inheritDoc}
889      */
890     public void flush() {
891         writer.flush();
892     }
893 
894     /**
895      * {@inheritDoc}
896      */
897     public void close() {
898         writer.close();
899 
900         init();
901     }
902 
903     // ----------------------------------------------------------------------
904     // Private methods
905     // ----------------------------------------------------------------------
906 
907     /**
908      * Escape special characters in a text in APT:
909      *
910      * <pre>
911      * \~, \=, \-, \+, \*, \[, \], \<, \>, \{, \}, \\
912      * </pre>
913      *
914      * @param text the String to escape, may be null
915      * @return the text escaped, "" if null String input
916      */
917     private static String escapeAPT(String text) {
918         if (text == null) {
919             return "";
920         }
921 
922         int length = text.length();
923         StringBuilder buffer = new StringBuilder(length);
924 
925         for (int i = 0; i < length; ++i) {
926             char c = text.charAt(i);
927             switch (c) { // 0080
928                 case '\\':
929                 case '~':
930                 case '=':
931                 case '-':
932                 case '+':
933                 case '*':
934                 case '[':
935                 case ']':
936                 case '<':
937                 case '>':
938                 case '{':
939                 case '}':
940                     buffer.append('\\');
941                     buffer.append(c);
942                     break;
943                 default:
944                     buffer.append(c);
945             }
946         }
947 
948         return buffer.toString();
949     }
950 }