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    
022    package org.biojava3.core.util;
023    
024    import java.io.IOException;
025    import java.io.PrintWriter;
026    import java.util.ArrayList;
027    import java.util.HashMap;
028    import java.util.Iterator;
029    import java.util.LinkedList;
030    import java.util.List;
031    import 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    
041    public 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<String, String> namespacePrefixes = new HashMap<String, String>();
050        private int namespaceSeed = 0;
051        private LinkedList<List<String>> namespaceBindings = new LinkedList<List<String>>();
052        private List<String> namespacesDeclared = new ArrayList<String>();
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<String> nsi = namespacesDeclared.iterator(); nsi.hasNext(); ) {
076                    String nsURI = 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<String> bindings = namespaceBindings.getLast();
114            if (bindings == null) {
115                bindings = new ArrayList<String>();
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 = 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 = 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<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        public void closeTag(String nsURI, String localName)
204            throws IOException
205        {
206            String prefix = 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    }