001/*
002 *                    BioJava development code
003 *
004 * This code may be freely distributed and modified under the
005 * terms of the GNU Lesser General Public Licence.  This should
006 * be distributed with the code.  If you do not have a copy,
007 * see:
008 *
009 *      http://www.gnu.org/copyleft/lesser.html
010 *
011 * Copyright for this code is held jointly by the individual
012 * authors.  These should be listed in @author doc comments.
013 *
014 * For more information on the BioJava project and its aims,
015 * or to join the biojava-l mailing list, visit the home page
016 * at:
017 *
018 *      http://www.biojava.org/
019 *
020 */
021
022package org.biojava.nbio.core.util;
023
024import java.io.IOException;
025import java.io.PrintWriter;
026import java.util.*;
027
028/**
029 * Implementation of XMLWriter which emits nicely formatted documents
030 * to a PrintWriter.
031 *
032 * @author Thomas Down
033 * @since 1.3
034 */
035
036public class PrettyXMLWriter implements XMLWriter {
037        private int indentUnit = 2;
038
039        private PrintWriter writer;
040        private boolean isOpeningTag = false;
041        private boolean afterNewline = true;
042        private int indent = 0;
043
044        private Map<String, String> namespacePrefixes = new HashMap<String, String>();
045        private int namespaceSeed = 0;
046        private LinkedList<List<String>> namespaceBindings = new LinkedList<List<String>>();
047        private List<String> namespacesDeclared = new ArrayList<String>();
048
049        public PrettyXMLWriter(PrintWriter writer) {
050                this.writer = writer;
051        }
052
053        @Override
054        public void declareNamespace(String nsURI, String prefixHint)
055                throws IOException
056        {
057                if (!namespacePrefixes.containsKey(nsURI)) {
058                        if (isOpeningTag) {
059                                String prefix = allocPrefix(nsURI);
060                                attribute("xmlns:" + prefix, nsURI);
061                        } else {
062                                namespacesDeclared.add(nsURI);
063                        }
064                }
065        }
066
067        private void handleDeclaredNamespaces()
068                throws IOException
069        {
070                if (namespacesDeclared.size() == 0) {
071                        for (Iterator<String> nsi = namespacesDeclared.iterator(); nsi.hasNext(); ) {
072                                String nsURI = nsi.next();
073                                if (!namespacePrefixes.containsKey(nsURI)) {
074                                        String prefix = allocPrefix(nsURI);
075                                        attribute("xmlns:" + prefix, nsURI);
076                                }
077                        }
078                        namespacesDeclared.clear();
079                }
080        }
081
082        protected void writeIndent()
083                throws IOException
084        {
085                for (int i = 0; i < indent * indentUnit; ++i) {
086                        writer.write(' ');
087                }
088        }
089
090        private void _openTag()
091                throws IOException
092        {
093                if (isOpeningTag) {
094                        writer.println('>');
095                        afterNewline = true;
096                }
097                if (afterNewline) {
098                        writeIndent();
099                }
100                indent++;
101                isOpeningTag = true;
102                afterNewline = false;
103                namespaceBindings.add(null);
104        }
105
106        private String allocPrefix(String nsURI) {
107                String prefix = "ns" + (++namespaceSeed);
108                namespacePrefixes.put(nsURI, prefix);
109                List<String> bindings = namespaceBindings.getLast();
110                if (bindings == null) {
111                        bindings = new ArrayList<String>();
112                        namespaceBindings.removeLast();
113                        namespaceBindings.add(bindings);
114                }
115                bindings.add(nsURI);
116                return prefix;
117        }
118
119        @Override
120        public void openTag(String nsURI, String localName)
121                throws IOException
122        {
123                if (nsURI == null || nsURI.length() == 0)
124                {
125                        throw new IOException("Invalid namespace URI: "+nsURI);
126                }
127                _openTag();
128                boolean alloced = false;
129                String prefix = namespacePrefixes.get(nsURI);
130                if (prefix == null) {
131                        prefix = allocPrefix(nsURI);
132                        alloced = true;
133                }
134                writer.print('<');
135                writer.print(prefix);
136                writer.print(':');
137                writer.print(localName);
138                if (alloced) {
139                        attribute("xmlns:" + prefix, nsURI);
140                }
141                handleDeclaredNamespaces();
142        }
143
144        @Override
145        public void openTag(String qName)
146                throws IOException
147        {
148                _openTag();
149                writer.print('<');
150                writer.print(qName);
151                handleDeclaredNamespaces();
152        }
153
154        @Override
155        public void attribute(String nsURI, String localName, String value)
156                throws IOException
157        {
158                if (! isOpeningTag) {
159                        throw new IOException("attributes must follow an openTag");
160                }
161
162                String prefix = namespacePrefixes.get(nsURI);
163                if (prefix == null) {
164                        prefix = allocPrefix(nsURI);
165                        attribute("xmlns:" + prefix, nsURI);
166                }
167
168                writer.print(' ');
169                writer.print(prefix);
170                writer.print(':');
171                writer.print(localName);
172                writer.print("=\"");
173                printAttributeValue(value);
174                writer.print('"');
175        }
176
177        @Override
178        public void attribute(String qName, String value)
179                throws IOException
180        {
181                if (! isOpeningTag) {
182                        throw new IOException("attributes must follow an openTag");
183                }
184
185                writer.print(' ');
186                writer.print(qName);
187                writer.print("=\"");
188                printAttributeValue(value);
189                writer.print('"');
190        }
191
192        private void _closeTag() {
193                isOpeningTag = false;
194                afterNewline = true;
195                List<String> hereBindings = namespaceBindings.removeLast();
196                if (hereBindings != null) {
197                        for (Iterator<String> bi = hereBindings.iterator(); bi.hasNext(); ) {
198                                namespacePrefixes.remove(bi.next());
199                        }
200                }
201        }
202
203        @Override
204        public void closeTag(String nsURI, String localName)
205                throws IOException
206        {
207                String prefix = namespacePrefixes.get(nsURI);
208                if (prefix == null) {
209                        throw new IOException("Assertion failed: unknown namespace when closing tag");
210                }
211                indent--;
212
213                if (isOpeningTag) {
214                        writer.println(" />");
215                } else {
216                        if (afterNewline) {
217                                writeIndent();
218                        }
219                        writer.print("</");
220                        writer.print(prefix);
221                        writer.print(':');
222                        writer.print(localName);
223                        writer.println('>');
224                }
225                _closeTag();
226        }
227
228        @Override
229        public void closeTag(String qName)
230                throws IOException
231        {
232                indent--;
233
234                if (isOpeningTag) {
235                        writer.println(" />");
236                } else {
237                        if (afterNewline) {
238                                writeIndent();
239                        }
240                        writer.print("</");
241                        writer.print(qName);
242                        writer.println('>');
243                }
244                _closeTag();
245        }
246
247        @Override
248        public void println(String data)
249                throws IOException
250        {
251        if (isOpeningTag) {
252                writer.println('>');
253                isOpeningTag = false;
254        }
255        printChars(data);
256        writer.println();
257        afterNewline = true;
258        }
259
260        @Override
261        public void print(String data)
262                throws IOException
263        {
264        if (isOpeningTag) {
265                writer.print('>');
266                isOpeningTag = false;
267        }
268        printChars(data);
269        afterNewline = false;
270        }
271
272        @Override
273        public void printRaw(String data)
274                throws IOException
275        {
276        writer.println(data);
277        }
278
279        protected void printChars(String data)
280                throws IOException
281        {
282        if (data == null) {
283                printChars("null");
284                return;
285        }
286
287        for (int pos = 0; pos < data.length(); ++pos) {
288                char c = data.charAt(pos);
289                if (c == '<' || c == '>' || c == '&') {
290                numericalEntity(c);
291                } else {
292                writer.write(c);
293                }
294        }
295        }
296
297        protected void printAttributeValue(String data)
298                throws IOException
299        {
300        if (data == null) {
301                printAttributeValue("null");
302                return;
303        }
304
305        for (int pos = 0; pos < data.length(); ++pos) {
306                char c = data.charAt(pos);
307                if (c == '<' || c == '>' || c == '&' || c == '"') {
308                numericalEntity(c);
309                } else {
310                writer.write(c);
311                }
312        }
313        }
314
315        protected void numericalEntity(char c)
316                throws IOException
317        {
318        writer.print("&#");
319        writer.print((int) c);
320        writer.print(';');
321        }
322
323        @Override
324        public void close()
325                throws IOException
326        {
327                writer.close();
328        }
329}