|
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 java.nio.charset.Charset; |
|
27 import java.util.EmptyStackException; |
|
28 import java.util.HashMap; |
|
29 import java.util.LinkedHashMap; |
|
30 import java.util.Map; |
|
31 import java.util.Map.Entry; |
|
32 import java.util.logging.Level; |
|
33 import java.util.logging.Logger; |
|
34 |
|
35 /** |
|
36 * |
|
37 * @author Ing. František Kučera (frantovo.cz) |
|
38 */ |
|
39 public abstract class AbstractXmlFormatter extends AbstractFormatter { |
|
40 |
|
41 private static final Logger log = Logger.getLogger(AbstractXmlFormatter.class.getName()); |
|
42 public static final String PROPERTY_COLORFUL = "color"; |
|
43 public static final String PROPERTY_INDENT = "indent"; |
|
44 private static final TerminalColor ELEMENT_COLOR = TerminalColor.Magenta; |
|
45 private static final TerminalColor ATTRIBUTE_NAME_COLOR = TerminalColor.Green; |
|
46 private static final TerminalColor ATTRIBUTE_VALUE_COLOR = TerminalColor.Yellow; |
|
47 private static final TerminalColor XML_DECLARATION_COLOR = TerminalColor.Red; |
|
48 private Stack<QName> treePosition = new Stack<>(); |
|
49 private final ColorfulPrintWriter out; |
|
50 private final String indent; |
|
51 |
|
52 public AbstractXmlFormatter(FormatterContext formatterContext) { |
|
53 super(formatterContext); |
|
54 boolean colorful = formatterContext.getProperties().getBoolean(PROPERTY_COLORFUL, false); |
|
55 out = new ColorfulPrintWriter(formatterContext.getOutputStream(), false, colorful); |
|
56 indent = formatterContext.getProperties().getString(PROPERTY_INDENT, "\t"); |
|
57 |
|
58 if (!indent.matches("\\s*")) { |
|
59 log.log(Level.WARNING, "Setting indent to „{0}“ is weird & freaky; in hex: {1}", new Object[]{indent, toHex(indent.getBytes())}); |
|
60 } |
|
61 |
|
62 } |
|
63 |
|
64 protected void printStartDocument() { |
|
65 out.print(XML_DECLARATION_COLOR, "<?xml version=\"1.0\" encoding=\"" + Charset.defaultCharset().name() + "\"?>"); |
|
66 } |
|
67 |
|
68 protected void printEndDocument() { |
|
69 out.println(); |
|
70 out.flush(); |
|
71 if (!treePosition.empty()) { |
|
72 throw new IllegalStateException("Some elements are not closed: " + treePosition); |
|
73 } |
|
74 } |
|
75 |
|
76 protected void printStartElement(QName element) { |
|
77 printStartElement(element, null); |
|
78 } |
|
79 |
|
80 protected Map<QName, String> singleAttribute(QName name, String value) { |
|
81 Map<QName, String> attributes = new HashMap<>(1); |
|
82 attributes.put(name, value); |
|
83 return attributes; |
|
84 } |
|
85 |
|
86 protected void printStartElement(QName element, Map<QName, String> attributes) { |
|
87 printStartElement(element, attributes, false); |
|
88 } |
|
89 |
|
90 /** |
|
91 * @param empty whether element should be closed <codfe>… /></code> (has no content, do not |
|
92 * call {@linkplain #printEndElement()}) |
|
93 */ |
|
94 private void printStartElement(QName element, Map<QName, String> attributes, boolean empty) { |
|
95 printIndent(); |
|
96 |
|
97 out.print(ELEMENT_COLOR, "<" + toString(element)); |
|
98 |
|
99 if (attributes != null) { |
|
100 for (Entry<QName, String> attribute : attributes.entrySet()) { |
|
101 out.print(" "); |
|
102 out.print(ATTRIBUTE_NAME_COLOR, toString(attribute.getKey())); |
|
103 out.print("="); |
|
104 out.print(ATTRIBUTE_VALUE_COLOR, '"' + escapeXmlAttribute(attribute.getValue()) + '"'); |
|
105 } |
|
106 } |
|
107 |
|
108 if (empty) { |
|
109 out.print(ELEMENT_COLOR, "/>"); |
|
110 } else { |
|
111 out.print(ELEMENT_COLOR, ">"); |
|
112 treePosition.add(element); |
|
113 } |
|
114 |
|
115 out.flush(); |
|
116 } |
|
117 |
|
118 /** |
|
119 * Prints text node wrapped in given element without indenting the text and adding line breaks |
|
120 * (useful for short texts). |
|
121 * |
|
122 * @param attributes use {@linkplain LinkedHashMap} to preserve attributes order |
|
123 */ |
|
124 protected void printTextElement(QName element, Map<QName, String> attributes, String text) { |
|
125 printStartElement(element, attributes); |
|
126 printText(text, false); |
|
127 printEndElement(false); |
|
128 } |
|
129 |
|
130 protected void printEmptyElement(QName element, Map<QName, String> attributes) { |
|
131 printStartElement(element, attributes, true); |
|
132 } |
|
133 |
|
134 protected void printEndElement() { |
|
135 printEndElement(true); |
|
136 } |
|
137 |
|
138 private void printEndElement(boolean indent) { |
|
139 try { |
|
140 QName name = treePosition.pop(); |
|
141 |
|
142 if (indent) { |
|
143 printIndent(); |
|
144 } |
|
145 |
|
146 out.print(ELEMENT_COLOR, "</" + toString(name) + ">"); |
|
147 out.flush(); |
|
148 |
|
149 } catch (EmptyStackException e) { |
|
150 throw new IllegalStateException("No more elements to end.", e); |
|
151 } |
|
152 } |
|
153 |
|
154 protected void printText(String s) { |
|
155 printText(s, true); |
|
156 } |
|
157 |
|
158 private void printText(String s, boolean indent) { |
|
159 if (indent) { |
|
160 printIndent(); |
|
161 } |
|
162 out.print(escapeXmlText(s)); |
|
163 out.flush(); |
|
164 } |
|
165 |
|
166 protected void printIndent() { |
|
167 out.println(); |
|
168 for (int i = 0; i < treePosition.size(); i++) { |
|
169 out.print(indent); |
|
170 } |
|
171 } |
|
172 |
|
173 protected static QName qname(String name) { |
|
174 return new QName(name); |
|
175 } |
|
176 |
|
177 protected static QName qname(String prefix, String name) { |
|
178 return new QName(null, name, prefix); |
|
179 } |
|
180 |
|
181 private String toString(QName name) { |
|
182 if (isEmpty(name.getPrefix(), true)) { |
|
183 return escapeName(name.getLocalPart()); |
|
184 } else { |
|
185 return escapeName(name.getPrefix()) + ":" + escapeName(name.getLocalPart()); |
|
186 } |
|
187 } |
|
188 |
|
189 private String escapeName(String s) { |
|
190 // TODO: avoid ugly values in <name name="…"/> |
|
191 return s; |
|
192 } |
|
193 |
|
194 private static String escapeXmlText(String s) { |
|
195 return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); |
|
196 // Not needed: |
|
197 // return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'"); |
|
198 } |
|
199 |
|
200 /** |
|
201 * Expects attribute values enclosed in "quotes" not 'apostrophes'. |
|
202 */ |
|
203 private static String escapeXmlAttribute(String s) { |
|
204 return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """); |
|
205 } |
|
206 } |