001/**
002 *
003 * Copyright 2018 Florian Schmaus
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jxmpp.xml.splitter;
018
019import java.io.IOException;
020
021import org.jxmpp.xml.splitter.XmlSplitter.State;
022
023public class XmlPrettyPrinter extends XmlPrinter {
024
025        private final int indent;
026        private final int attributeIndent;
027        private final int tabWidth;
028
029        private final PrettyPrintedXmlChunkWithCurrentPartCallback newChunkCallback;
030        private final PrettyPrintedXmlPartCallback newPartCallback;
031        private final PrettyPrintedXmlChunkSink prettyWriter;
032
033        private StringBuilder currentPart;
034        private StringBuilder currentChunk;
035        private StringBuilder currentChunkWithCurrentPart;
036
037        /**
038         * Construct a new XML pretty printer.
039         *
040         * @param partCallback a part callback.
041         */
042        public XmlPrettyPrinter(PrettyPrintedXmlPartCallback partCallback) {
043                this(builder().setPartCallback(partCallback));
044        }
045
046        /**
047         * Construct a new XML pretty printer.
048         *
049         * @param prettyWriter a writer for the pretty printed XML stream.
050         */
051        public XmlPrettyPrinter(PrettyPrintedXmlChunkSink prettyWriter) {
052                this(builder().setPrettyWriter(prettyWriter));
053        }
054
055        private XmlPrettyPrinter(Builder builder) {
056                this.indent = builder.indent;
057                this.attributeIndent = builder.attributeIndent;
058                this.tabWidth = builder.tabWidth;
059                this.newChunkCallback = builder.newChunkCallback;
060                this.newPartCallback = builder.newPartCallback;
061                this.prettyWriter = builder.prettyWriter;
062        }
063
064        @Override
065        void onChunkStart() {
066                if (newChunkCallback != null) {
067                        currentChunkWithCurrentPart = new StringBuilder(currentPart.length() + 1024);
068                        currentChunkWithCurrentPart.append(currentPart);
069                        currentChunkWithCurrentPart.append('[');
070                }
071
072                if (prettyWriter != null) {
073                        currentChunk = new StringBuilder(1024);
074                }
075        }
076
077        @Override
078        void onChunkEnd() {
079                if (newChunkCallback != null) {
080                        currentChunkWithCurrentPart.append(']');
081                        newChunkCallback.onPrettyPrintedXmlChunk(currentChunkWithCurrentPart);
082                        currentChunkWithCurrentPart = null;
083                }
084
085                if (prettyWriter != null) {
086                        prettyWriter.sink(currentChunk);
087                        currentChunk = null;
088                }
089        }
090
091        @SuppressWarnings("incomplete-switch")
092        @Override
093        void onNextChar(char c, int depth, State initialState, State currentState) throws IOException {
094                final boolean stateChange = initialState != currentState;
095                final StringBuilder sb = new StringBuilder(stateChange ? 16 : 1);
096
097                if (stateChange) {
098                        boolean deferredLeftAngle = false;
099                        int indent = 0;
100                        switch (currentState) {
101                        case TAG_LEFT_ANGLE_BRACKET:
102                                // Note that we return here because we need to see if this is a start tag or end tag.
103                                return;
104                        case END_TAG_SOLIDUS:
105                                indent = getElementIndent(depth - 1);
106                                deferredLeftAngle = true;
107                                break;
108                        case IN_TAG_NAME:
109                                indent = getElementIndent(depth);
110                                deferredLeftAngle = true;
111                                break;
112                        case IN_ATTRIBUTE_NAME:
113                                if (attributeIndent > 0) {
114                                        indent = getAttributeIndent(depth);
115                                }
116                                break;
117                        case START:
118                                indent = getElementIndent(depth);
119                                break;
120                        }
121
122                        if (indent > 0 || deferredLeftAngle) {
123                                sb.append('\n');
124                        }
125
126                        appendIndent(sb, indent);
127
128                        if (deferredLeftAngle) {
129                                sb.append('<');
130                        }
131                }
132
133                sb.append(c);
134
135                if (currentChunkWithCurrentPart != null) {
136                        currentChunkWithCurrentPart.append(sb);
137                }
138                if (newPartCallback != null) {
139                        if (currentPart == null) {
140                                currentPart = new StringBuilder(1024);
141                        }
142                        currentPart.append(sb);
143                }
144                if (prettyWriter != null) {
145                        currentChunk.append(sb);
146                }
147        }
148
149        @Override
150        void onCompleteElement() {
151                if (newPartCallback == null) {
152                        return;
153                }
154                if (currentPart.charAt(0) == '\n') {
155                        currentPart.deleteCharAt(0);
156                }
157                newPartCallback.onPrettyPrintedXmlPart(currentPart);
158                currentPart = null;
159        }
160
161        private int getElementIndent(int depth) {
162                return indent * depth;
163        }
164
165        private int getAttributeIndent(int depth) {
166                return getElementIndent(depth) + attributeIndent;
167        }
168
169        private void appendIndent(StringBuilder sb, int indent) {
170                int spaces = indent;
171                if (tabWidth > 0) {
172                        spaces = indent % tabWidth;
173                        int tabs = indent / tabWidth;
174                        for (int i = 0; i < tabs; i++) {
175                                sb.append('\t');
176                        }
177                }
178                for (int i = 0; i < spaces; i++) {
179                        sb.append(' ');
180                }
181        }
182
183        public interface PrettyPrintedXmlChunkWithCurrentPartCallback {
184
185                /**
186                 * Invoked after the XML pretty printer handled a chunk. The pretty printed chunk will contain the current part
187                 * and the newly handled chunk enclosing in '[' and ']'.
188                 *
189                 * @param chunk the state of the current part with the newly handled chunk marked.
190                 */
191                void onPrettyPrintedXmlChunk(StringBuilder chunk);
192        }
193
194        public interface PrettyPrintedXmlPartCallback {
195
196                /**
197                 * Invoked after a part was completed.
198                 *
199                 * @param part the pretty printed part.
200                 */
201                void onPrettyPrintedXmlPart(StringBuilder part);
202        }
203
204        /**
205         * A functional interface which acts as sink for character sequences.
206         */
207        public interface PrettyPrintedXmlChunkSink {
208
209                /**
210                 * Sink of the pretty printed XML chunk.
211                 *
212                 * @param stringBuilder a StringBuilder containing the pretty printed XML of the current chunk.
213                 */
214                void sink(StringBuilder stringBuilder);
215        }
216
217        /**
218         * Create a new builder.
219         *
220         * @return a new builder.
221         */
222        public static Builder builder() {
223                return new Builder();
224        }
225
226        public static final class Builder {
227                private int indent = 2;
228                private int attributeIndent;
229                private int tabWidth;
230
231                private PrettyPrintedXmlChunkWithCurrentPartCallback newChunkCallback;
232                private PrettyPrintedXmlPartCallback newPartCallback;
233                private PrettyPrintedXmlChunkSink prettyWriter;
234
235                private Builder() {
236                }
237
238                /**
239                 * Set the indent for elements in whitespace characters.
240                 *
241                 * @param indent the indent for elements in whitespace characters.
242                 * @return a reference to this builders.
243                 */
244                public Builder setIndent(int indent) {
245                        ensureNotNegative(indent);
246
247                        this.indent = indent;
248                        return this;
249                }
250
251                /**
252                 * Set the attribute indent in whitespace characters. Use a value smaller one to disable attribute indentation.
253                 *
254                 * @param attributeIndent the attribute indent in whitespace characters.
255                 * @return a reference to this builder.
256                 */
257                public Builder setAttributeIndent(int attributeIndent) {
258                        ensureNotNegative(attributeIndent);
259
260                        this.attributeIndent = attributeIndent;
261                        return this;
262                }
263
264                /**
265                 * Set the tab width in whitespace characters. Use a value smaller one to disable pretty printing with tabs.
266                 *
267                 * @param tabWidth the tab width in whitespace characters.
268                 * @return a reference to this builder.
269                 */
270                public Builder setTabWidth(int tabWidth) {
271                        ensureNotNegative(tabWidth);
272
273                        this.tabWidth = tabWidth;
274                        return this;
275                }
276
277                /**
278                 * Set a chunk callback.
279                 *
280                 * @param chunkCallback the chunk callback.
281                 * @return a reference to this builder.
282                 */
283                public Builder setChunkCallback(PrettyPrintedXmlChunkWithCurrentPartCallback chunkCallback) {
284                        this.newChunkCallback = chunkCallback;
285                        return this;
286                }
287
288                /**
289                 * Set a part callback.
290                 *
291                 * @param partCallback the part callback.
292                 * @return a reference to this builder.
293                 */
294                public Builder setPartCallback(PrettyPrintedXmlPartCallback partCallback) {
295                        this.newPartCallback = partCallback;
296                        return this;
297                }
298
299                /**
300                 * Set a {@link PrettyPrintedXmlChunkSink} for the pretty printed XML stream.
301                 *
302                 * @param prettyWriter the writer to pretty print to.
303                 * @return a reference to this builder.
304                 */
305                public Builder setPrettyWriter(PrettyPrintedXmlChunkSink prettyWriter) {
306                        this.prettyWriter = prettyWriter;
307                        return this;
308                }
309
310                /**
311                 * Build an XML pretty printer.
312                 *
313                 * @return the newly build XML pretty printer.
314                 */
315                public XmlPrettyPrinter build() {
316                        return new XmlPrettyPrinter(this);
317                }
318
319                private static void ensureNotNegative(int i) {
320                        if (i < 0) {
321                                throw new IllegalArgumentException();
322                        }
323                }
324        }
325}