1 /** |
|
2 * SQL-DK |
|
3 * Copyright © 2014 František Kučera (frantovo.cz) |
|
4 * |
|
5 * This program is free software: you can redistribute it and/or modify |
|
6 * it under the terms of the GNU General Public License as published by |
|
7 * the Free Software Foundation, either version 3 of the License, or |
|
8 * (at your option) any later version. |
|
9 * |
|
10 * This program is distributed in the hope that it will be useful, |
|
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
13 * GNU General Public License for more details. |
|
14 * |
|
15 * You should have received a copy of the GNU General Public License |
|
16 * along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
17 */ |
|
18 package info.globalcode.sql.dk.formatting; |
|
19 |
|
20 import info.globalcode.sql.dk.ColorfulPrintWriter; |
|
21 import info.globalcode.sql.dk.ColorfulPrintWriter.TerminalColor; |
|
22 import java.util.Stack; |
|
23 import javax.xml.namespace.QName; |
|
24 import static info.globalcode.sql.dk.Functions.isEmpty; |
|
25 import static info.globalcode.sql.dk.Functions.toHex; |
|
26 import info.globalcode.sql.dk.configuration.PropertyDeclaration; |
|
27 import static info.globalcode.sql.dk.formatting.CommonProperties.COLORFUL; |
|
28 import static info.globalcode.sql.dk.formatting.CommonProperties.COLORFUL_DESCRIPTION; |
|
29 import java.nio.charset.Charset; |
|
30 import java.util.EmptyStackException; |
|
31 import java.util.HashMap; |
|
32 import java.util.LinkedHashMap; |
|
33 import java.util.Map; |
|
34 import java.util.Map.Entry; |
|
35 import java.util.logging.Level; |
|
36 import java.util.logging.Logger; |
|
37 |
|
38 /** |
|
39 * <p> |
|
40 * Provides helper methods for printing pretty intended and optionally colorful (syntax highlighted) |
|
41 * XML output. |
|
42 * </p> |
|
43 * |
|
44 * <p> |
|
45 * Must be used with care – bad usage can lead to invalid XML (e.g. using undeclared namespaces). |
|
46 * </p> |
|
47 * |
|
48 * @author Ing. František Kučera (frantovo.cz) |
|
49 */ |
|
50 @PropertyDeclaration(name = COLORFUL, defaultValue = "false", type = Boolean.class, description = COLORFUL_DESCRIPTION) |
|
51 @PropertyDeclaration(name = AbstractXmlFormatter.PROPERTY_INDENT, defaultValue = AbstractXmlFormatter.PROPERTY_INDENT_DEFAULT, type = String.class, description = "tab or sequence of spaces used for indentation of nested elements") |
|
52 @PropertyDeclaration(name = AbstractXmlFormatter.PROPERTY_INDENT_TEXT, defaultValue = "true", type = Boolean.class, description = "whether text with line breaks should be indented; if not original whitespace will be preserved.") |
|
53 public abstract class AbstractXmlFormatter extends AbstractFormatter { |
|
54 |
|
55 private static final Logger log = Logger.getLogger(AbstractXmlFormatter.class.getName()); |
|
56 public static final String PROPERTY_INDENT = "indent"; |
|
57 protected static final String PROPERTY_INDENT_DEFAULT = "\t"; |
|
58 public static final String PROPERTY_INDENT_TEXT = "indentText"; |
|
59 private static final TerminalColor ELEMENT_COLOR = TerminalColor.Magenta; |
|
60 private static final TerminalColor ATTRIBUTE_NAME_COLOR = TerminalColor.Green; |
|
61 private static final TerminalColor ATTRIBUTE_VALUE_COLOR = TerminalColor.Yellow; |
|
62 private static final TerminalColor XML_DECLARATION_COLOR = TerminalColor.Red; |
|
63 private static final TerminalColor XML_DOCTYPE_COLOR = TerminalColor.Cyan; |
|
64 private Stack<QName> treePosition = new Stack<>(); |
|
65 private final ColorfulPrintWriter out; |
|
66 private final String indent; |
|
67 private final boolean indentText; |
|
68 |
|
69 public AbstractXmlFormatter(FormatterContext formatterContext) { |
|
70 super(formatterContext); |
|
71 boolean colorful = formatterContext.getProperties().getBoolean(COLORFUL, false); |
|
72 out = new ColorfulPrintWriter(formatterContext.getOutputStream(), false, colorful); |
|
73 indent = formatterContext.getProperties().getString(PROPERTY_INDENT, PROPERTY_INDENT_DEFAULT); |
|
74 indentText = formatterContext.getProperties().getBoolean(PROPERTY_INDENT_TEXT, true); |
|
75 |
|
76 if (!indent.matches("\\s*")) { |
|
77 log.log(Level.WARNING, "Setting indent to „{0}“ is weird & freaky; in hex: {1}", new Object[]{indent, toHex(indent.getBytes())}); |
|
78 } |
|
79 |
|
80 } |
|
81 |
|
82 protected void printStartDocument() { |
|
83 out.print(XML_DECLARATION_COLOR, "<?xml version=\"1.0\" encoding=\"" + Charset.defaultCharset().name() + "\"?>"); |
|
84 } |
|
85 |
|
86 protected void printDoctype(String doctype) { |
|
87 out.print(XML_DOCTYPE_COLOR, "\n<!DOCTYPE " + doctype + ">"); |
|
88 } |
|
89 |
|
90 protected void printEndDocument() { |
|
91 out.println(); |
|
92 out.flush(); |
|
93 if (!treePosition.empty()) { |
|
94 throw new IllegalStateException("Some elements are not closed: " + treePosition); |
|
95 } |
|
96 } |
|
97 |
|
98 protected void printStartElement(QName element) { |
|
99 printStartElement(element, null); |
|
100 } |
|
101 |
|
102 protected Map<QName, String> singleAttribute(QName name, String value) { |
|
103 Map<QName, String> attributes = new HashMap<>(2); |
|
104 attributes.put(name, value); |
|
105 return attributes; |
|
106 } |
|
107 |
|
108 protected void printStartElement(QName element, Map<QName, String> attributes) { |
|
109 printStartElement(element, attributes, false); |
|
110 } |
|
111 |
|
112 /** |
|
113 * @param empty whether element should be closed <codfe>… /></code> (has no content, do not |
|
114 * call {@linkplain #printEndElement()}) |
|
115 */ |
|
116 private void printStartElement(QName element, Map<QName, String> attributes, boolean empty) { |
|
117 printIndent(); |
|
118 |
|
119 out.print(ELEMENT_COLOR, "<" + toString(element)); |
|
120 |
|
121 if (attributes != null) { |
|
122 for (Entry<QName, String> attribute : attributes.entrySet()) { |
|
123 out.print(" "); |
|
124 out.print(ATTRIBUTE_NAME_COLOR, toString(attribute.getKey())); |
|
125 out.print("="); |
|
126 out.print(ATTRIBUTE_VALUE_COLOR, '"' + escapeXmlAttribute(attribute.getValue()) + '"'); |
|
127 } |
|
128 } |
|
129 |
|
130 if (empty) { |
|
131 out.print(ELEMENT_COLOR, "/>"); |
|
132 } else { |
|
133 out.print(ELEMENT_COLOR, ">"); |
|
134 treePosition.add(element); |
|
135 } |
|
136 |
|
137 out.flush(); |
|
138 } |
|
139 |
|
140 /** |
|
141 * Prints text node wrapped in given element without indenting the text and adding line breaks |
|
142 * (useful for short texts). |
|
143 * |
|
144 * @param attributes use {@linkplain LinkedHashMap} to preserve attributes order |
|
145 */ |
|
146 protected void printTextElement(QName element, Map<QName, String> attributes, String text) { |
|
147 printStartElement(element, attributes); |
|
148 |
|
149 String[] lines = text.split("\\n"); |
|
150 |
|
151 if (indentText && lines.length > 1) { |
|
152 for (String line : lines) { |
|
153 printText(line, true); |
|
154 } |
|
155 printEndElement(true); |
|
156 } else { |
|
157 /* |
|
158 * line breaks at the end of the text will be eaten – if you need them, use indentText = false |
|
159 */ |
|
160 if (lines.length == 1 && text.endsWith("\n")) { |
|
161 text = text.substring(0, text.length() - 1); |
|
162 } |
|
163 |
|
164 printText(text, false); |
|
165 printEndElement(false); |
|
166 } |
|
167 } |
|
168 |
|
169 protected void printEmptyElement(QName element, Map<QName, String> attributes) { |
|
170 printStartElement(element, attributes, true); |
|
171 } |
|
172 |
|
173 protected void printEndElement() { |
|
174 printEndElement(true); |
|
175 } |
|
176 |
|
177 private void printEndElement(boolean indent) { |
|
178 try { |
|
179 QName name = treePosition.pop(); |
|
180 |
|
181 if (indent) { |
|
182 printIndent(); |
|
183 } |
|
184 |
|
185 out.print(ELEMENT_COLOR, "</" + toString(name) + ">"); |
|
186 out.flush(); |
|
187 |
|
188 } catch (EmptyStackException e) { |
|
189 throw new IllegalStateException("No more elements to end.", e); |
|
190 } |
|
191 } |
|
192 |
|
193 protected void printText(String s, boolean indent) { |
|
194 if (indent) { |
|
195 printIndent(); |
|
196 } |
|
197 out.print(escapeXmlText(s)); |
|
198 out.flush(); |
|
199 } |
|
200 |
|
201 protected void printIndent() { |
|
202 out.println(); |
|
203 for (int i = 0; i < treePosition.size(); i++) { |
|
204 out.print(indent); |
|
205 } |
|
206 } |
|
207 |
|
208 protected static QName qname(String name) { |
|
209 return new QName(name); |
|
210 } |
|
211 |
|
212 protected static QName qname(String prefix, String name) { |
|
213 return new QName(null, name, prefix); |
|
214 } |
|
215 |
|
216 private String toString(QName name) { |
|
217 if (isEmpty(name.getPrefix(), true)) { |
|
218 return escapeName(name.getLocalPart()); |
|
219 } else { |
|
220 return escapeName(name.getPrefix()) + ":" + escapeName(name.getLocalPart()); |
|
221 } |
|
222 } |
|
223 |
|
224 private String escapeName(String s) { |
|
225 // TODO: avoid ugly values in <name name="…"/> |
|
226 return s; |
|
227 } |
|
228 |
|
229 private static String escapeXmlText(String s) { |
|
230 return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); |
|
231 // Not needed: |
|
232 // return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'"); |
|
233 } |
|
234 |
|
235 /** |
|
236 * Expects attribute values enclosed in "quotes" not 'apostrophes'. |
|
237 */ |
|
238 private static String escapeXmlAttribute(String s) { |
|
239 return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """); |
|
240 } |
|
241 } |
|