OSDN Git Service

3eb2e1c3649c9c8242f497b1e658edfe9ec7fe65
[xerial/xerial-core.git] / src / main / java / org / xerial / silk / SilkWriter.java
1 /*--------------------------------------------------------------------------
2  *  Copyright 2009 Taro L. Saito
3  *
4  *  Licensed under the Apache License, Version 2.0 (the "License");
5  *  you may not use this file except in compliance with the License.
6  *  You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  *  Unless required by applicable law or agreed to in writing, software
11  *  distributed under the License is distributed on an "AS IS" BASIS,
12  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  *  See the License for the specific language governing permissions and
14  *  limitations under the License.
15  *--------------------------------------------------------------------------*/
16 //--------------------------------------
17 // XerialJ
18 //
19 // SilkWriter.java
20 // Since: May 28, 2009 5:04:34 PM
21 //
22 // $URL$
23 // $Author$
24 //--------------------------------------
25 package org.xerial.silk;
26
27 import java.io.OutputStream;
28 import java.io.OutputStreamWriter;
29 import java.io.PrintWriter;
30 import java.io.Writer;
31 import java.util.ArrayList;
32 import java.util.HashSet;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.Set;
36 import java.util.Map.Entry;
37 import java.util.regex.Matcher;
38 import java.util.regex.Pattern;
39
40 import org.xerial.core.XerialError;
41 import org.xerial.core.XerialErrorCode;
42 import org.xerial.lens.ObjectLens;
43 import org.xerial.lens.impl.ParameterGetter;
44 import org.xerial.util.StringUtil;
45 import org.xerial.util.bean.TypeInfo;
46
47 /**
48  * Supporting class for generating Silk data. This class is not thread-safe,
49  * i.e, concurrent access to this class may cause invalid Silk format.
50  * 
51  * 
52  * @author leo
53  * 
54  */
55 public class SilkWriter {
56
57     private final SilkWriter parent;
58     private final PrintWriter out;
59     private Set<SilkWriter> childWriterSet = new HashSet<SilkWriter>();
60
61     private final String contextNodeName;
62
63     /**
64      * Silk output format configurations
65      * 
66      * @author leo
67      * 
68      */
69     public static class FormatConfig {
70
71         // indentation
72         public int indentWidth = 2;
73         public boolean indentBeforeDataLine = false;
74         public boolean indentCommentLine = false;
75
76         // comment
77         public boolean insertSpaceAfterCommentSymbol = true;
78
79         // comma
80         public boolean insertSpaceAfterComma = true;
81
82         // colon
83         public boolean insertSpaceBeforeColon = false;
84         public boolean insertSpaceAfterColon = true;
85         /**
86          * insert tab after colon symbol (intermediate node only)
87          */
88         public boolean insertTagAfterColon = false;
89
90         // colon (inline-node)
91         public boolean insertSpaceBeforeAttributeColon = false;
92         public boolean insertSpaceAfterAttributeColon = false;
93         public boolean insertTagAfterAttributeColon = false;
94
95         // preamble
96         public boolean insertSpaceAfterPreambleSymbol = true;
97
98         // parenthesis 
99         public boolean insertSpaceOutsideOfParen = false;
100         public boolean insertSpaceInsideOfParen = false;
101     }
102
103     private FormatConfig formatConfig = new FormatConfig();
104     private final int levelOffset;
105     private boolean isUsable = true;
106
107     private SyntaxType nodeValueSyntaxType = SyntaxType.DEFAULT;
108
109     private int numAttribute = 0;
110
111     public SilkWriter(Writer out) {
112         this.out = new PrintWriter(out);
113         this.parent = null;
114         this.levelOffset = 0;
115         this.contextNodeName = null;
116     }
117
118     public SilkWriter(OutputStream out) {
119         this(new PrintWriter(new OutputStreamWriter(out)));
120     }
121
122     private SilkWriter(String contextNodeName, SilkWriter parent) {
123         this.parent = parent;
124         this.out = parent.out;
125         this.levelOffset = parent.levelOffset + 1;
126         this.contextNodeName = contextNodeName;
127         this.formatConfig = parent.formatConfig;
128     }
129
130     public void setFormatConfig(FormatConfig config) {
131         if (config == null)
132             throw new NullPointerException("config is null");
133
134         this.formatConfig = config;
135     }
136
137     /**
138      * Get the parent writer
139      * 
140      * @return
141      */
142     public SilkWriter getParent() {
143         return parent;
144     }
145
146     public String getContextNodeName() {
147         return contextNodeName;
148     }
149
150     public void flush() {
151         this.out.flush();
152     }
153
154     public void endDocument() {
155         usabilityCheck();
156         attributeParenCloseCheck(true);
157         flush();
158     }
159
160     /**
161      * Close the writer (or output stream) inside the SilkWriter
162      */
163     public void close() {
164         endDocument();
165         this.out.close();
166     }
167
168     public SilkWriter preamble() {
169         out.print("%");
170         if (formatConfig.insertSpaceAfterPreambleSymbol)
171             out.print(" ");
172         out.print("silk");
173
174         openParen();
175         keyAndValue("version", "1.0");
176         closeParen();
177
178         return this;
179     }
180
181     /**
182      * output comment line
183      * 
184      * @param comment
185      * @return
186      */
187     public SilkWriter commentLine(String comment) {
188
189         usabilityCheck();
190         // before generating comment line, close the opened attribute parenthesis
191         attributeParenCloseCheck(true);
192
193         String[] comments = comment.split("(\\r\\n|\\r|\\n)");
194
195         int index = 0;
196         for (String each : comments) {
197             if (formatConfig.indentCommentLine)
198                 printIndent();
199
200             out.print("#");
201
202             if (formatConfig.insertSpaceAfterCommentSymbol)
203                 out.print(" ");
204
205             if (index < comments.length - 1)
206                 out.println(each);
207             else
208                 out.print(each);
209             index++;
210         }
211         return this;
212     }
213
214     private void printIndent() {
215         final int indentLevel = levelOffset;
216         for (int i = 0; i < indentLevel; ++i) {
217             for (int w = 0; w < formatConfig.indentWidth; ++w)
218                 out.append(" ");
219         }
220     }
221
222     private void invalidateChildWriters() {
223         for (SilkWriter each : childWriterSet) {
224             each.invalidate();
225         }
226
227         childWriterSet.clear();
228     }
229
230     private void invalidate() {
231         attributeParenCloseCheck(false);
232         this.isUsable = false;
233     }
234
235     private void usabilityCheck() {
236         if (!this.isUsable)
237             throw new XerialError(XerialErrorCode.INVALID_USAGE, "This writer is no longer usable");
238
239         invalidateChildWriters();
240     }
241
242     private void openParen() {
243         if (formatConfig.insertSpaceOutsideOfParen)
244             out.print(" ");
245         out.print("(");
246         if (formatConfig.insertSpaceInsideOfParen)
247             out.print(" ");
248     }
249
250     private void closeParen() {
251         if (formatConfig.insertSpaceInsideOfParen)
252             out.print(" ");
253         out.print(")");
254         if (formatConfig.insertSpaceOutsideOfParen)
255             out.print(" ");
256     }
257
258     private void attributeParenCloseCheck(boolean insertNewline) {
259         if (numAttribute > 0) {
260             closeParen();
261
262             if (nodeValueSyntaxType == SyntaxType.TAB)
263                 out.print("|");
264         }
265
266         switch (nodeValueSyntaxType) {
267         case SEQUENCE:
268             out.print(">");
269             break;
270         }
271
272         if (insertNewline)
273             out.println();
274
275         numAttribute = 0;
276     }
277
278     /**
279      * Output a silk node, and gets a new SilkWriter for genarating attributes
280      * and child nodes
281      * 
282      * @param nodeName
283      * @return
284      */
285     public SilkWriter node(String nodeName) {
286         usabilityCheck();
287
288         attributeParenCloseCheck(true);
289
290         printNodeName(nodeName);
291         SilkWriter child = new SilkWriter(nodeName, this);
292         registChildWriter(child);
293         return child;
294     }
295
296     private void printNodeName(String nodeName) {
297         printIndent();
298         out.print("-");
299         if (nodeName != null)
300             out.print(nodeName);
301     }
302
303     public SilkWriter tabDataSchema(String nodeName) {
304         SilkWriter child = node(nodeName);
305         child.setNodeValueSyntax(SyntaxType.TAB);
306         return child;
307     }
308
309     public SilkWriter multilineData(String nodeName) {
310         SilkWriter child = node(nodeName);
311         child.setNodeValueSyntax(SyntaxType.SEQUENCE);
312         return child;
313     }
314
315     public static enum SyntaxType {
316         DEFAULT, TAB, SEQUENCE
317     }
318
319     public SilkWriter setNodeValueSyntax(SyntaxType type) {
320         this.nodeValueSyntaxType = type;
321         return this;
322     }
323
324     /**
325      * Remember the child writer
326      * 
327      * @param childWriter
328      */
329     private void registChildWriter(SilkWriter childWriter) {
330         childWriterSet.add(childWriter);
331     }
332
333     public SilkWriter attribute(String nodeName) {
334         return attribute(nodeName, null);
335     }
336
337     private void keyAndValue(String key, String value) {
338         out.print(key);
339         colonAndNodeValueForInlineNode(value);
340     }
341
342     public SilkWriter attribute(String nodeName, String nodeValue) {
343         usabilityCheck();
344
345         if (numAttribute == 0) {
346             openParen();
347         }
348         else
349             comma();
350
351         keyAndValue(nodeName, nodeValue);
352
353         numAttribute++;
354         return this;
355     }
356
357     public SilkWriter nodeValue(String value) {
358         usabilityCheck();
359         attributeParenCloseCheck(false);
360
361         colonAndNodeValue(value);
362         return this;
363     }
364
365     public SilkWriter leaf(String nodeName) {
366         return leaf(nodeName, null);
367     }
368
369     public SilkWriter leaf(String nodeName, String nodeValue) {
370         usabilityCheck();
371
372         attributeParenCloseCheck(true);
373
374         printIndent();
375         out.print("-");
376         out.print(nodeName);
377
378         colonAndNodeValue(nodeValue);
379
380         return this;
381     }
382
383     void comma() {
384         out.print(",");
385         if (formatConfig.insertSpaceAfterComma)
386             out.print(" ");
387     }
388
389     void colonAndNodeValueForInlineNode(String nodeValue) {
390         if (nodeValue != null) {
391             if (formatConfig.insertSpaceBeforeAttributeColon)
392                 out.print(" ");
393             out.print(":");
394             if (formatConfig.insertSpaceAfterAttributeColon)
395                 out.print(" ");
396             if (formatConfig.insertTagAfterAttributeColon)
397                 out.print("\t");
398             out.print(sanitizeInLineNodeValue(nodeValue));
399         }
400     }
401
402     static String sanitizeInLineNodeValue(String nodeValue) {
403
404         String mustBeEscaped = "\"";
405         nodeValue = nodeValue.replaceAll("\"", "\\\"");
406         // "[](){},:>*|")));
407         Pattern p = Pattern.compile(String.format("[%s]", "\\[\\](){},:>*|"));
408         Matcher m = p.matcher(nodeValue);
409         if (m.find()) {
410             nodeValue = StringUtil.doubleQuote(nodeValue);
411         }
412         return nodeValue;
413     }
414
415     /**
416      * Test whether this writer is usable or not
417      * 
418      * @return
419      */
420     public boolean isUsable() {
421         return isUsable;
422     }
423
424     void colonAndNodeValue(String nodeValue) {
425         if (nodeValue != null) {
426             if (formatConfig.insertSpaceBeforeColon)
427                 out.print(" ");
428             out.print(":");
429             if (formatConfig.insertSpaceAfterColon)
430                 out.print(" ");
431             if (formatConfig.insertTagAfterColon)
432                 out.print("\t");
433             out.print(nodeValue);
434         }
435     }
436
437     public SilkWriter dataLine(String dataLine) {
438         usabilityCheck();
439
440         attributeParenCloseCheck(true);
441
442         if (formatConfig.indentBeforeDataLine)
443             printIndent();
444
445         out.print(escapeDataLine(dataLine));
446
447         return this;
448     }
449
450     public SilkWriter text(String text) {
451         usabilityCheck();
452
453         attributeParenCloseCheck(true);
454
455         if (formatConfig.indentBeforeDataLine)
456             printIndent();
457
458         String line[] = text.split("\r?\n");
459         if (line == null)
460             return this;
461
462         for (String each : line) {
463             out.print(escapeDataLine(each));
464         }
465
466         return this;
467     }
468
469     public static String escapeText(String text) {
470         String[] line = text.split("\r?\n");
471         if (line == null)
472             return escapeDataLine(text);
473
474         List<String> buf = new ArrayList<String>();
475         for (String each : line) {
476             buf.add(escapeDataLine(each));
477         }
478         return StringUtil.join(buf, StringUtil.NEW_LINE);
479     }
480
481     private static Pattern leadingHyphen = Pattern.compile("\\s*-");
482
483     private static String escapeDataLine(String dataLine) {
484
485         if (dataLine == null)
486             return dataLine;
487
488         Matcher m = leadingHyphen.matcher(dataLine);
489         if (m.lookingAt()) {
490             int hyphenPos = m.end();
491             StringBuilder buf = new StringBuilder();
492             buf.append(dataLine.substring(0, hyphenPos - 1));
493             buf.append("\\");
494             buf.append(dataLine.substring(hyphenPos - 1));
495             return buf.toString();
496         }
497
498         // no change
499         return dataLine;
500     }
501
502     private <Value> SilkWriter leafObject(String leafNodeName, Value v) {
503         if (v == null)
504             return this;
505
506         if (TypeInfo.isBasicType(v.getClass())) {
507
508             if (parent != null)
509                 attribute(leafNodeName, v.toString());
510             else
511                 leaf(leafNodeName, v.toString());
512         }
513         else {
514             ObjectLens lens = ObjectLens.getObjectLens(v.getClass());
515             if (lens.hasAttributes()) {
516                 SilkWriter c = node(leafNodeName);
517                 c.toSilk(v);
518             }
519             else {
520                 if (parent != null)
521                     attribute(leafNodeName, v.toString());
522                 else
523                     leaf(leafNodeName, v.toString());
524             }
525         }
526         return this;
527     }
528
529     public SilkWriter toSilk(Object obj) {
530
531         if (obj == null)
532             return this;
533
534         usabilityCheck();
535
536         Class< ? > c = obj.getClass();
537
538         if (TypeInfo.isBasicType(c)) {
539             nodeValue(obj.toString());
540             return this;
541         }
542
543         ObjectLens lens = ObjectLens.getObjectLens(obj.getClass());
544
545         if (TypeInfo.isIterable(c)) {
546             Iterable< ? > collection = (Iterable< ? >) obj;
547             boolean hasAttributes = lens.hasAttributes();
548             if (hasAttributes) {
549                 outputParemters(lens, obj);
550             }
551
552             if (collection != null) {
553                 for (Object elem : collection) {
554                     SilkWriter w = node(null);
555                     w.toSilk(elem);
556                 }
557             }
558         }
559         else if (TypeInfo.isMap(c)) {
560             Map< ? , ? > map = (Map< ? , ? >) obj;
561             boolean hasAttributes = lens.hasAttributes();
562
563             if (hasAttributes) {
564                 outputParemters(lens, obj);
565             }
566
567             if (!map.isEmpty()) {
568
569                 String mapElemName = getContextNodeName();
570                 if (mapElemName == null)
571                     mapElemName = "entry";
572
573                 for (Entry< ? , ? > each : map.entrySet()) {
574                     Object key = each.getKey();
575                     Object value = each.getValue();
576
577                     if (TypeInfo.isBasicType(key.getClass())) {
578                         leafObject(key.toString(), value);
579                     }
580                     else {
581                         SilkWriter w = node(mapElemName);
582                         w.node("key").toSilk(key);
583                         w.node("value").toSilk(value);
584                     }
585                 }
586             }
587         }
588         else {
589             if (lens.hasAttributes())
590                 outputParemters(lens, obj);
591             else
592                 out.print(obj.toString());
593         }
594
595         attributeParenCloseCheck(false);
596
597         return this;
598     }
599
600     private void outputParemters(ObjectLens lens, Object obj) {
601
602         List<ParameterGetter> getterContainer = lens.getGetterContainer();
603         List<ParameterGetter> postponedParameters = new ArrayList<ParameterGetter>();
604
605         // output attribute-like parameters first
606         for (ParameterGetter getter : getterContainer) {
607
608             Class< ? > c = getter.getReturnType();
609             if (TypeInfo.isBasicType(c)) {
610                 leafObject(getter.getParamName(), getter.get(obj));
611             }
612             else {
613                 if (TypeInfo.isIterable(c) || TypeInfo.isMap(c)) {
614                     postponedParameters.add(getter);
615                 }
616                 else {
617                     ObjectLens paramLens = ObjectLens.getObjectLens(c);
618                     if (paramLens.hasAttributes())
619                         postponedParameters.add(getter);
620                     else
621                         leafObject(getter.getParamName(), getter.get(obj));
622                 }
623             }
624
625         }
626
627         for (ParameterGetter getter : postponedParameters) {
628
629             Class< ? > c = getter.getReturnType();
630             if (TypeInfo.isIterable(c)) {
631                 Iterable< ? > collection = (Iterable< ? >) getter.get(obj);
632
633                 if (collection != null) {
634                     for (Object elem : collection) {
635                         SilkWriter w = node(getter.getParamName());
636                         w.toSilk(elem);
637                     }
638                 }
639             }
640             else if (TypeInfo.isMap(c)) {
641                 Map< ? , ? > map = (Map< ? , ? >) getter.get(obj);
642
643                 if (!map.isEmpty()) {
644
645                     String mapElemName = getter.getParamName();
646
647                     for (Entry< ? , ? > each : map.entrySet()) {
648                         Object key = each.getKey();
649                         Object value = each.getValue();
650
651                         if (TypeInfo.isBasicType(key.getClass())) {
652                             leafObject(key.toString(), value);
653                         }
654                         else {
655                             SilkWriter w = node(mapElemName);
656                             w.node("key").toSilk(key);
657                             w.node("value").toSilk(value);
658                         }
659                     }
660                 }
661             }
662             else {
663                 leafObject(getter.getParamName(), getter.get(obj));
664             }
665         }
666     }
667
668 }