1 /*
2 * XML Format Maven Plugin (https://github.com/acegi/xml-format-maven-plugin)
3 *
4 * Copyright 2011-2025 Acegi Technology Pty Limited.
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * https://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18 package au.com.acegi.xmlformat;
19
20 import java.io.IOException;
21 import java.io.OutputStream;
22 import java.io.UnsupportedEncodingException;
23 import java.util.StringTokenizer;
24
25 import org.apache.commons.lang3.StringUtils;
26 import org.dom4j.Node;
27 import org.dom4j.io.XMLWriter;
28
29 /**
30 * Subclass of {@link XMLWriter} that preserves blank lines in outupt (at most one of them, among two subsequent tags).
31 */
32 class BlankLinesWriter extends XMLWriter {
33
34 BlankLinesWriter(final OutputStream out, final XmlOutputFormat fmt) throws UnsupportedEncodingException {
35 super(out, fmt);
36 }
37
38 @Override
39 protected void writeString(final String text) throws IOException {
40 if (text == null || text.length() == 0) {
41 return;
42 }
43
44 String input = text;
45 if (isEscapeText()) {
46 input = escapeElementEntities(text);
47 }
48
49 if (getOutputFormat().isTrimText()) {
50 boolean first = true;
51 final StringTokenizer tokenizer = new StringTokenizer(input, " \t\r\f");
52
53 final NewLinesHandler newLinesHandler = new NewLinesHandler();
54 while (tokenizer.hasMoreTokens()) {
55 final String token = tokenizer.nextToken();
56
57 // Only if more tokens exist, continue
58 if (newLinesHandler.processToken(token, tokenizer.hasMoreTokens())) {
59 continue;
60 }
61
62 if (first) {
63 first = false;
64 if (lastOutputNodeType == Node.TEXT_NODE) {
65 writer.write(" ");
66 }
67 } else {
68 writer.write(" ");
69 }
70
71 writer.write(token.trim());
72 lastOutputNodeType = Node.TEXT_NODE;
73 }
74 newLinesHandler.finished();
75 } else {
76 lastOutputNodeType = Node.TEXT_NODE;
77 writer.write(input);
78 }
79 }
80
81 private final class NewLinesHandler {
82 private int newLinesCount;
83
84 /**
85 * Processes the token, counting the newlines and producing at most one in output.
86 *
87 * @param token
88 * The token to be written
89 * @param hasMoreTokens
90 * Does more tokens exist
91 *
92 * @return True if the token needs to be skipped (it's a newline or a set of newlines)
93 *
94 * @throws IOException
95 * If an I/O error occurs.
96 */
97 private boolean processToken(final String token, final boolean hasMoreTokens) throws IOException {
98 final int tokenNewLines = StringUtils.countMatches(token, '\n');
99 if (tokenNewLines > 0) {
100 newLinesCount += tokenNewLines;
101 return hasMoreTokens;
102 }
103 if (newLinesCount > 1) {
104 writer.write("\n");
105 newLinesCount = 0;
106 }
107 return false;
108 }
109
110 /**
111 * Marks the end of token streams, allows to emit a last newlines if the last tokens were all newlines.
112 *
113 * @throws IOException
114 * If an I/O error occurs.
115 */
116 private void finished() throws IOException {
117 if (newLinesCount > 1) {
118 writer.write("\n");
119 }
120 }
121 }
122 }