View Javadoc
1   /*
2    * SPDX-License-Identifier: Apache-2.0
3    * See LICENSE file for details.
4    *
5    * Copyright 2025-2026 Hazendaz
6    * Copyright 2011-2025 Acegi Technology Pty Limited.
7    */
8   package au.com.acegi.xmlformat;
9   
10  import java.io.IOException;
11  import java.io.OutputStream;
12  import java.io.UnsupportedEncodingException;
13  import java.util.StringTokenizer;
14  
15  import org.apache.commons.lang3.StringUtils;
16  import org.dom4j.Node;
17  import org.dom4j.io.XMLWriter;
18  
19  /**
20   * Subclass of {@link XMLWriter} that preserves blank lines in outupt (at most one of them, among two subsequent tags).
21   */
22  class BlankLinesWriter extends XMLWriter {
23  
24      BlankLinesWriter(final OutputStream out, final XmlOutputFormat fmt) throws UnsupportedEncodingException {
25          super(out, fmt);
26      }
27  
28      @Override
29      protected void writeString(final String text) throws IOException {
30          if (text == null || text.length() == 0) {
31              return;
32          }
33  
34          String input = text;
35          if (isEscapeText()) {
36              input = escapeElementEntities(text);
37          }
38  
39          if (getOutputFormat().isTrimText()) {
40              boolean first = true;
41              final StringTokenizer tokenizer = new StringTokenizer(input, " \t\r\f");
42  
43              final NewLinesHandler newLinesHandler = new NewLinesHandler();
44              while (tokenizer.hasMoreTokens()) {
45                  final String token = tokenizer.nextToken();
46  
47                  // Only if more tokens exist, continue
48                  if (newLinesHandler.processToken(token, tokenizer.hasMoreTokens())) {
49                      continue;
50                  }
51  
52                  if (first) {
53                      first = false;
54                      if (lastOutputNodeType == Node.TEXT_NODE) {
55                          writer.write(" ");
56                      }
57                  } else {
58                      writer.write(" ");
59                  }
60  
61                  writer.write(token.trim());
62                  lastOutputNodeType = Node.TEXT_NODE;
63              }
64              newLinesHandler.finished();
65          } else {
66              lastOutputNodeType = Node.TEXT_NODE;
67              writer.write(input);
68          }
69      }
70  
71      private final class NewLinesHandler {
72          private int newLinesCount;
73  
74          /**
75           * Processes the token, counting the newlines and producing at most one in output.
76           *
77           * @param token
78           *            The token to be written
79           * @param hasMoreTokens
80           *            Does more tokens exist
81           *
82           * @return True if the token needs to be skipped (it's a newline or a set of newlines)
83           *
84           * @throws IOException
85           *             If an I/O error occurs.
86           */
87          private boolean processToken(final String token, final boolean hasMoreTokens) throws IOException {
88              final int tokenNewLines = StringUtils.countMatches(token, '\n');
89              if (tokenNewLines > 0) {
90                  newLinesCount += tokenNewLines;
91                  return hasMoreTokens;
92              }
93              if (newLinesCount > 1) {
94                  writer.write("\n");
95                  newLinesCount = 0;
96              }
97              return false;
98          }
99  
100         /**
101          * Marks the end of token streams, allows to emit a last newlines if the last tokens were all newlines.
102          *
103          * @throws IOException
104          *             If an I/O error occurs.
105          */
106         private void finished() throws IOException {
107             if (newLinesCount > 1) {
108                 writer.write("\n");
109             }
110         }
111     }
112 }