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.utils.xml;
023
024import java.io.IOException;
025import java.io.PrintWriter;
026import java.util.ArrayList;
027import java.util.HashMap;
028import java.util.Iterator;
029import java.util.LinkedList;
030import java.util.List;
031import java.util.Map;
032
033/**
034 * Implementation of XMLWriter which emits nicely formatted documents
035 * to a PrintWriter.
036 *
037 * @author Thomas Down
038 * @since 1.3
039 */
040
041public class PrettyXMLWriter implements XMLWriter {
042    private int indentUnit = 2;
043
044    private PrintWriter writer;
045    private boolean isOpeningTag = false;
046    private boolean afterNewline = true;
047    private int indent = 0;
048    
049    private Map namespacePrefixes = new HashMap();
050    private int namespaceSeed = 0;
051    private LinkedList namespaceBindings = new LinkedList();
052    private List namespacesDeclared = new ArrayList();
053
054    public PrettyXMLWriter(PrintWriter writer) {
055        this.writer = writer;
056    }
057
058    public void declareNamespace(String nsURI, String prefixHint) 
059        throws IOException
060    {
061        if (!namespacePrefixes.containsKey(nsURI)) {
062            if (isOpeningTag == true) {
063                String prefix = allocPrefix(nsURI);
064                attribute("xmlns:" + prefix, nsURI);
065            } else {
066                namespacesDeclared.add(nsURI);
067            }
068        }
069    }
070    
071    private void handleDeclaredNamespaces() 
072        throws IOException
073    {
074        if (namespacesDeclared.size() == 0) {
075            for (Iterator nsi = namespacesDeclared.iterator(); nsi.hasNext(); ) {
076                String nsURI = (String) nsi.next();
077                if (!namespacePrefixes.containsKey(nsURI)) {
078                    String prefix = allocPrefix(nsURI);
079                    attribute("xmlns:" + prefix, nsURI);
080                }
081            }
082            namespacesDeclared.clear();
083        }
084    }
085    
086    protected void writeIndent()
087        throws IOException
088    {
089        for (int i = 0; i < indent * indentUnit; ++i) {
090            writer.write(' ');
091        }
092    }
093
094    private void _openTag()
095        throws IOException
096    {
097        if (isOpeningTag) {
098            writer.println('>');
099            afterNewline = true;
100        }
101        if (afterNewline) {
102            writeIndent();
103        }
104        indent++;
105        isOpeningTag = true;
106        afterNewline = false;
107        namespaceBindings.add(null);
108    }
109    
110    private String allocPrefix(String nsURI) {
111        String prefix = "ns" + (++namespaceSeed);
112        namespacePrefixes.put(nsURI, prefix);
113        List bindings = (List) namespaceBindings.getLast();
114        if (bindings == null) {
115            bindings = new ArrayList();
116            namespaceBindings.removeLast();
117            namespaceBindings.add(bindings);
118        }
119        bindings.add(nsURI);
120        return prefix;
121    }
122    
123    public void openTag(String nsURI, String localName)
124        throws IOException
125    {
126        if (nsURI == null || nsURI.length() == 0)
127        {
128                throw new IOException("Invalid namespace URI: "+nsURI);
129        }
130        _openTag();
131        boolean alloced = false;
132        String prefix = (String) namespacePrefixes.get(nsURI);
133        if (prefix == null) {
134            prefix = allocPrefix(nsURI);
135            alloced = true;
136        }
137        writer.print('<');
138        writer.print(prefix);
139        writer.print(':');
140        writer.print(localName);
141        if (alloced) {
142            attribute("xmlns:" + prefix, nsURI);
143        }
144        handleDeclaredNamespaces();
145    }
146    
147    public void openTag(String qName)
148        throws IOException
149    {
150        _openTag();
151        writer.print('<');
152        writer.print(qName);
153        handleDeclaredNamespaces();
154    }
155
156    public void attribute(String nsURI, String localName, String value)
157        throws IOException
158    {
159        if (! isOpeningTag) {
160            throw new IOException("attributes must follow an openTag");
161        }
162
163        String prefix = (String) namespacePrefixes.get(nsURI);
164        if (prefix == null) {
165            prefix = allocPrefix(nsURI);
166            attribute("xmlns:" + prefix, nsURI);
167        }
168        
169        writer.print(' ');
170        writer.print(prefix);
171        writer.print(':');
172        writer.print(localName);
173        writer.print("=\"");
174        printAttributeValue(value);
175        writer.print('"');
176    }
177    
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 hereBindings = (List) namespaceBindings.removeLast();
196        if (hereBindings != null) {
197            for (Iterator bi = hereBindings.iterator(); bi.hasNext(); ) {
198                namespacePrefixes.remove(bi.next());
199            }
200        }
201    }
202    
203    public void closeTag(String nsURI, String localName)
204        throws IOException
205    {
206        String prefix = (String) namespacePrefixes.get(nsURI);
207        if (prefix == null) {
208            throw new IOException("Assertion failed: unknown namespace when closing tag");
209        }
210        indent--;
211
212        if (isOpeningTag) {
213            writer.println(" />");
214        } else {
215            if (afterNewline) {
216                writeIndent();
217            }
218            writer.print("</");
219            writer.print(prefix);
220            writer.print(':');
221            writer.print(localName);
222            writer.println('>');
223        }
224        _closeTag();
225    }
226    
227    public void closeTag(String qName) 
228        throws IOException
229    {
230        indent--;
231
232        if (isOpeningTag) {
233            writer.println(" />");
234        } else {
235            if (afterNewline) {
236                writeIndent();
237            }
238            writer.print("</");
239            writer.print(qName);
240            writer.println('>');
241        }
242        _closeTag();
243    }
244
245    public void println(String data)
246        throws IOException
247    {
248        if (isOpeningTag) {
249            writer.println('>');
250            isOpeningTag = false;
251        }
252        printChars(data);
253        writer.println();
254        afterNewline = true;
255    }
256
257    public void print(String data)
258        throws IOException
259    {
260        if (isOpeningTag) {
261            writer.print('>');
262            isOpeningTag = false;
263        }
264        printChars(data);
265        afterNewline = false;
266    }
267
268    public void printRaw(String data)
269        throws IOException
270    {
271        writer.println(data);
272    }
273
274    protected void printChars(String data) 
275        throws IOException
276    {
277        if (data == null) {
278            printChars("null");
279            return;
280        }
281
282        for (int pos = 0; pos < data.length(); ++pos) {
283            char c = data.charAt(pos);
284            if (c == '<' || c == '>' || c == '&') {
285                numericalEntity(c);
286            } else {
287                writer.write(c);
288            }
289        }
290    }
291
292    protected void printAttributeValue(String data) 
293        throws IOException
294    {
295        if (data == null) {
296            printAttributeValue("null");
297            return;
298        }
299
300        for (int pos = 0; pos < data.length(); ++pos) {
301            char c = data.charAt(pos);
302            if (c == '<' || c == '>' || c == '&' || c == '"') {
303                numericalEntity(c);
304            } else {
305                writer.write(c);
306            }
307        }
308    }
309
310    protected void numericalEntity(char c)
311        throws IOException
312    {
313        writer.print("&#");
314        writer.print((int) c);
315        writer.print(';');
316    }
317    
318    public void close()
319        throws IOException
320    {
321        writer.close();
322    }
323}