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