1 package com.fasterxml.jackson.dataformat.xml.util;
2
3 import java.io.IOException;
4 import java.math.BigDecimal;
5 import java.math.BigInteger;
6 import java.util.Arrays;
7
8 import javax.xml.stream.XMLStreamException;
9
10 import org.codehaus.stax2.XMLStreamWriter2;
11
12 import com.fasterxml.jackson.core.*;
13 import com.fasterxml.jackson.core.util.Instantiatable;
14
15 import com.fasterxml.jackson.dataformat.xml.XmlPrettyPrinter;
16 import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator;
17
18 /**
19  * Indentation to use with XML is different from JSON, because JSON
20  * requires use of separator characters and XML just basic whitespace.
21  *<p>
22  * Note that only a subset of methods of {@link PrettyPrinter} actually
23  * get called by {@link ToXmlGenerator}; because of this, implementation
24  * is bit briefer (and uglier...).
25  */

26 public class DefaultXmlPrettyPrinter
27     implements XmlPrettyPrinter, Instantiatable<DefaultXmlPrettyPrinter>,
28         java.io.Serializable
29 {
30     private static final long serialVersionUID = 1L; // since 2.6
31
32     /**
33      * Interface that defines objects that can produce indentation used
34      * to separate object entries and array values. Indentation in this
35      * context just means insertion of white space, independent of whether
36      * linefeeds are output.
37      */

38     public interface Indenter
39     {
40         public void writeIndentation(JsonGenerator g, int level) throws IOException;
41
42         public void writeIndentation(XMLStreamWriter2 sw, int level) throws XMLStreamException;
43
44         /**
45          * @return True if indenter is considered inline (does not add linefeeds),
46          *   false otherwise
47          */

48         public boolean isInline();
49     }
50
51     /*
52     /**********************************************************
53     /* Configuration
54     /**********************************************************
55      */

56
57     /**
58      * By default, let's use only spaces to separate array values.
59      */

60     protected Indenter _arrayIndenter = new FixedSpaceIndenter();
61
62     /**
63      * By default, let's use linefeed-adding indenter for separate
64      * object entries. We'll further configure indenter to use
65      * system-specific linefeeds, and 2 spaces per level (as opposed to,
66      * say, single tabs)
67      */

68     protected Indenter _objectIndenter = new Lf2SpacesIndenter();
69
70     // // // Config, other white space configuration
71
72     /**
73      * By default we will add spaces around colons used to
74      * separate object fields and values.
75      * If disabled, will not use spaces around colon.
76      */

77     protected boolean _spacesInObjectEntries = true;
78
79     /*
80     /**********************************************************
81     /* State
82     /**********************************************************
83     */

84     
85     /**
86      * Number of open levels of nesting. Used to determine amount of
87      * indentation to use.
88      */

89     protected transient int _nesting = 0;
90
91     /**
92      * Marker flag set on start element, and cleared if an end element
93      * is encountered. Used for suppressing indentation to allow empty
94      * elements.
95      * 
96      * @since 2.3
97      */

98     protected transient boolean _justHadStartElement;
99     
100     /*
101     /**********************************************************
102     /* Life-cycle (construct, configure)
103     /**********************************************************
104     */

105
106     public DefaultXmlPrettyPrinter() { }
107
108     protected DefaultXmlPrettyPrinter(DefaultXmlPrettyPrinter base)
109     {
110         _arrayIndenter = base._arrayIndenter;
111         _objectIndenter = base._objectIndenter;
112         _spacesInObjectEntries = base._spacesInObjectEntries;
113         _nesting = base._nesting;
114     }
115
116     public void indentArraysWith(Indenter i)
117     {
118         _arrayIndenter = (i == null) ? new NopIndenter() : i;
119     }
120
121     public void indentObjectsWith(Indenter i)
122     {
123         _objectIndenter = (i == null) ? new NopIndenter() : i;
124     }
125
126     public void spacesInObjectEntries(boolean b) { _spacesInObjectEntries = b; }
127
128     /*
129     /**********************************************************
130     /* Instantiatable impl
131     /**********************************************************
132      */

133     
134     @Override
135     public DefaultXmlPrettyPrinter createInstance() {
136         return new DefaultXmlPrettyPrinter(this);
137     }
138
139     /*
140     /**********************************************************
141     /* PrettyPrinter impl
142     /**********************************************************
143      */

144
145     @Override
146     public void writeRootValueSeparator(JsonGenerator gen) throws IOException {
147         // Not sure if this should ever be applicable; but if multiple roots were allowed, we'd use linefeed
148         gen.writeRaw('\n');
149     }
150     
151     /*
152     /**********************************************************
153     /* Array values
154     /**********************************************************
155      */

156     
157     @Override
158     public void beforeArrayValues(JsonGenerator gen) throws IOException {
159         // never called for ToXmlGenerator
160     }
161
162     @Override
163     public void writeStartArray(JsonGenerator gen) throws IOException {
164         // anything to do here?
165     }
166
167     @Override
168     public void writeArrayValueSeparator(JsonGenerator gen)  throws IOException {
169         // never called for ToXmlGenerator
170     }
171
172     @Override
173     public void writeEndArray(JsonGenerator gen, int nrOfValues) throws IOException {
174         // anything to do here?
175     }
176     
177     /*
178     /**********************************************************
179     /* Object values
180     /**********************************************************
181      */

182
183     @Override
184     public void beforeObjectEntries(JsonGenerator gen)
185         throws IOException, JsonGenerationException
186     {
187         // never called for ToXmlGenerator
188     }
189
190     @Override
191     public void writeStartObject(JsonGenerator gen) throws IOException
192     {
193         if (!_objectIndenter.isInline()) {
194             if (_nesting > 0) {
195                 _objectIndenter.writeIndentation(gen, _nesting);
196             }
197             ++_nesting;
198         }
199         _justHadStartElement = true;
200         ((ToXmlGenerator) gen)._handleStartObject();
201     }
202
203     @Override
204     public void writeObjectEntrySeparator(JsonGenerator gen) throws IOException {
205         // never called for ToXmlGenerator
206     }
207
208     @Override
209     public void writeObjectFieldValueSeparator(JsonGenerator gen) throws IOException {
210         // never called for ToXmlGenerator
211     }
212     
213     @Override
214     public void writeEndObject(JsonGenerator gen, int nrOfEntries) throws IOException
215     {
216         if (!_objectIndenter.isInline()) {
217             --_nesting;
218         }
219         // for empty elements, no need for linefeeds etc:
220         if (_justHadStartElement) {
221             _justHadStartElement = false;
222         } else {
223             _objectIndenter.writeIndentation(gen, _nesting);
224         }
225         ((ToXmlGenerator) gen)._handleEndObject();
226     }
227     
228     /*
229     /**********************************************************
230     /* XML-specific additions
231     /**********************************************************
232      */

233
234     @Override
235     public void writeStartElement(XMLStreamWriter2 sw,
236             String nsURI, String localName) throws XMLStreamException
237     {
238         if (!_objectIndenter.isInline()) {
239             if (_justHadStartElement) {
240                 _justHadStartElement = false;
241             }
242             _objectIndenter.writeIndentation(sw, _nesting);
243             ++_nesting;
244         }
245         sw.writeStartElement(nsURI, localName);
246         _justHadStartElement = true;        
247     }
248
249     @Override
250     public void writeEndElement(XMLStreamWriter2 sw, int nrOfEntries) throws XMLStreamException
251     {
252         if (!_objectIndenter.isInline()) {
253             --_nesting;
254         }
255         // for empty elements, no need for linefeeds etc:
256         if (_justHadStartElement) {
257             _justHadStartElement = false;
258         } else {
259             _objectIndenter.writeIndentation(sw, _nesting);
260         }
261         sw.writeEndElement();
262     }
263     
264     @Override
265     public void writeLeafElement(XMLStreamWriter2 sw,
266             String nsURI, String localName, String text, boolean isCData)
267           throws XMLStreamException
268     {
269         if (!_objectIndenter.isInline()) {
270             _objectIndenter.writeIndentation(sw, _nesting);
271         }
272         sw.writeStartElement(nsURI, localName);
273         if(isCData) {
274             sw.writeCData(text);
275         } else {
276             sw.writeCharacters(text);
277         }
278         sw.writeEndElement();
279         _justHadStartElement = false;
280     }
281
282     @Override
283     public void writeLeafElement(XMLStreamWriter2 sw,
284             String nsURI, String localName,
285             char[] buffer, int offset, int len, boolean isCData)
286         throws XMLStreamException
287     {
288         if (!_objectIndenter.isInline()) {
289             _objectIndenter.writeIndentation(sw, _nesting);
290         }
291         sw.writeStartElement(nsURI, localName);
292         if(isCData) {
293             sw.writeCData(buffer, offset, len);
294         } else {
295             sw.writeCharacters(buffer, offset, len);
296         }
297         sw.writeEndElement();
298         _justHadStartElement = false;
299     }
300     
301     @Override
302     public void writeLeafElement(XMLStreamWriter2 sw,
303             String nsURI, String localName, boolean value)
304           throws XMLStreamException
305     {
306         if (!_objectIndenter.isInline()) {
307             _objectIndenter.writeIndentation(sw, _nesting);
308         }
309         sw.writeStartElement(nsURI, localName);
310         sw.writeBoolean(value);
311         sw.writeEndElement();
312         _justHadStartElement = false;
313     }
314     
315     @Override
316     public void writeLeafElement(XMLStreamWriter2 sw,
317             String nsURI, String localName, int value)
318         throws XMLStreamException
319     {
320         if (!_objectIndenter.isInline()) {
321             _objectIndenter.writeIndentation(sw, _nesting);
322         }
323         sw.writeStartElement(nsURI, localName);
324         sw.writeInt(value);
325         sw.writeEndElement();
326         _justHadStartElement = false;
327     }
328
329     @Override
330     public void writeLeafElement(XMLStreamWriter2 sw,
331             String nsURI, String localName, long value)
332         throws XMLStreamException
333     {
334         if (!_objectIndenter.isInline()) {
335             _objectIndenter.writeIndentation(sw, _nesting);
336         }
337         sw.writeStartElement(nsURI, localName);
338         sw.writeLong(value);
339         sw.writeEndElement();
340         _justHadStartElement = false;
341     }
342
343     @Override
344     public void writeLeafElement(XMLStreamWriter2 sw,
345             String nsURI, String localName, double value)
346           throws XMLStreamException
347     {
348         if (!_objectIndenter.isInline()) {
349             _objectIndenter.writeIndentation(sw, _nesting);
350         }
351         sw.writeStartElement(nsURI, localName);
352         sw.writeDouble(value);
353         sw.writeEndElement();
354         _justHadStartElement = false;
355     }
356
357     @Override
358     public void writeLeafElement(XMLStreamWriter2 sw,
359             String nsURI, String localName, float value)
360           throws XMLStreamException
361     {
362         if (!_objectIndenter.isInline()) {
363             _objectIndenter.writeIndentation(sw, _nesting);
364         }
365         sw.writeStartElement(nsURI, localName);
366         sw.writeFloat(value);
367         sw.writeEndElement();
368         _justHadStartElement = false;
369     }
370     
371     @Override
372     public void writeLeafElement(XMLStreamWriter2 sw,
373             String nsURI, String localName, BigInteger value)
374         throws XMLStreamException
375     {
376         if (!_objectIndenter.isInline()) {
377             _objectIndenter.writeIndentation(sw, _nesting);
378         }
379         sw.writeStartElement(nsURI, localName);
380         sw.writeInteger(value);
381         sw.writeEndElement();
382         _justHadStartElement = false;
383     }
384
385     @Override
386     public void writeLeafElement(XMLStreamWriter2 sw,
387             String nsURI, String localName, BigDecimal value)
388           throws XMLStreamException
389     {
390         if (!_objectIndenter.isInline()) {
391             _objectIndenter.writeIndentation(sw, _nesting);
392         }
393         sw.writeStartElement(nsURI, localName);
394         sw.writeDecimal(value);
395         sw.writeEndElement();
396         _justHadStartElement = false;
397     }
398
399     @Override
400     public void writeLeafElement(XMLStreamWriter2 sw,
401             String nsURI, String localName,
402             byte[] data, int offset, int len)
403         throws XMLStreamException
404     {
405         if (!_objectIndenter.isInline()) {
406             _objectIndenter.writeIndentation(sw, _nesting);
407         }
408         sw.writeStartElement(nsURI, localName);
409         sw.writeBinary(data, offset, len);
410         sw.writeEndElement();
411         _justHadStartElement = false;
412     }
413
414     @Override
415     public void writeLeafNullElement(XMLStreamWriter2 sw,
416             String nsURI, String localName)
417         throws XMLStreamException
418     {
419         if (!_objectIndenter.isInline()) {
420             _objectIndenter.writeIndentation(sw, _nesting);
421         }
422         sw.writeEmptyElement(nsURI, localName);
423         _justHadStartElement = false;
424     }
425
426     @Override // since 2.7
427     public void writePrologLinefeed(XMLStreamWriter2 sw) throws XMLStreamException
428     {
429         // 06-Dec-2015, tatu: Alternatively could try calling `writeSpace()`...
430         sw.writeRaw(Lf2SpacesIndenter.SYSTEM_LINE_SEPARATOR);
431     }
432
433     /*
434     /**********************************************************
435     /* Helper classes
436     /* (note: copied from jackson-core to avoid dependency;
437     /* allow local changes)
438     /**********************************************************
439      */

440
441     /**
442      * Dummy implementation that adds no indentation whatsoever
443      */

444     protected static class NopIndenter
445         implements Indenter, java.io.Serializable
446     {
447         private static final long serialVersionUID = 1L;
448
449         public NopIndenter() { }
450         @Override public void writeIndentation(JsonGenerator jg, int level) { }
451         @Override public boolean isInline() { return true; }
452         @Override public void writeIndentation(XMLStreamWriter2 sw, int level) { }
453     }
454
455     /**
456      * This is a very simple indenter that only every adds a
457      * single space for indentation. It is used as the default
458      * indenter for array values.
459      */

460     protected static class FixedSpaceIndenter
461         implements Indenter, java.io.Serializable
462     {
463         private static final long serialVersionUID = 1L;
464
465         public FixedSpaceIndenter() { }
466
467         @Override
468         public void writeIndentation(XMLStreamWriter2 sw, int level)
469             throws XMLStreamException
470         {
471             sw.writeRaw(" ");
472         }
473         
474         @Override
475         public void writeIndentation(JsonGenerator g, int level) throws IOException
476         {
477             g.writeRaw(' ');
478         }
479
480         @Override
481         public boolean isInline() { return true; }
482     }
483
484     /**
485      * Default linefeed-based indenter uses system-specific linefeeds and
486      * 2 spaces for indentation per level.
487      */

488     protected static class Lf2SpacesIndenter
489         implements Indenter, java.io.Serializable
490     {
491         private static final long serialVersionUID = 1L;
492
493         final static String SYSTEM_LINE_SEPARATOR;
494         static {
495             String lf = null;
496             try {
497                 lf = System.getProperty("line.separator");
498             } catch (Throwable t) { } // access exception?
499             SYSTEM_LINE_SEPARATOR = (lf == null) ? "\n" : lf;
500         }
501
502         final static int SPACE_COUNT = 64;
503         final static char[] SPACES = new char[SPACE_COUNT];
504         static {
505             Arrays.fill(SPACES, ' ');
506         }
507
508         public Lf2SpacesIndenter() { }
509
510         @Override
511         public boolean isInline() { return false; }
512
513         @Override
514         public void writeIndentation(XMLStreamWriter2 sw, int level) throws XMLStreamException
515         {
516             sw.writeRaw(SYSTEM_LINE_SEPARATOR);
517             level += level; // 2 spaces per level
518             while (level > SPACE_COUNT) { // should never happen but...
519                 sw.writeRaw(SPACES, 0, SPACE_COUNT); 
520                 level -= SPACES.length;
521             }
522             sw.writeRaw(SPACES, 0, level);
523         }
524         
525         @Override
526         public void writeIndentation(JsonGenerator jg, int level) throws IOException
527         {
528             jg.writeRaw(SYSTEM_LINE_SEPARATOR);
529             level += level; // 2 spaces per level
530             while (level > SPACE_COUNT) { // should never happen but...
531                 jg.writeRaw(SPACES, 0, SPACE_COUNT); 
532                 level -= SPACES.length;
533             }
534             jg.writeRaw(SPACES, 0, level);
535         }
536     }
537 }
538