001    /*
002     * Copyright (c) 2007 Henri Sivonen
003     * Copyright (c) 2008-2009 Mozilla Foundation
004     *
005     * Permission is hereby granted, free of charge, to any person obtaining a 
006     * copy of this software and associated documentation files (the "Software"), 
007     * to deal in the Software without restriction, including without limitation 
008     * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
009     * and/or sell copies of the Software, and to permit persons to whom the 
010     * Software is furnished to do so, subject to the following conditions:
011     *
012     * The above copyright notice and this permission notice shall be included in 
013     * all copies or substantial portions of the Software.
014     *
015     * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
016     * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
017     * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
018     * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
019     * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
020     * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
021     * DEALINGS IN THE SOFTWARE.
022     */
023    
024    package nu.validator.htmlparser.sax;
025    
026    import java.io.IOException;
027    import java.io.OutputStream;
028    import java.io.OutputStreamWriter;
029    import java.io.UnsupportedEncodingException;
030    import java.io.Writer;
031    import java.nio.charset.Charset;
032    import java.nio.charset.CharsetEncoder;
033    import java.nio.charset.CodingErrorAction;
034    import java.util.HashMap;
035    import java.util.HashSet;
036    import java.util.LinkedList;
037    import java.util.Map;
038    import java.util.Set;
039    
040    import org.xml.sax.Attributes;
041    import org.xml.sax.ContentHandler;
042    import org.xml.sax.Locator;
043    import org.xml.sax.SAXException;
044    import org.xml.sax.ext.LexicalHandler;
045    
046    public class XmlSerializer implements ContentHandler, LexicalHandler {
047    
048        private final class PrefixMapping {
049            public final String uri;
050    
051            public final String prefix;
052    
053            /**
054             * @param uri
055             * @param prefix
056             */
057            public PrefixMapping(String uri, String prefix) {
058                this.uri = uri;
059                this.prefix = prefix;
060            }
061    
062            /**
063             * @see java.lang.Object#equals(java.lang.Object)
064             */
065            @Override public final boolean equals(Object obj) {
066                if (obj instanceof PrefixMapping) {
067                    PrefixMapping other = (PrefixMapping) obj;
068                    return this.prefix.equals(other.prefix);
069                } else {
070                    return false;
071                }
072            }
073    
074            /**
075             * @see java.lang.Object#hashCode()
076             */
077            @Override public final int hashCode() {
078                return prefix.hashCode();
079            }
080    
081        }
082    
083        private final class StackNode {
084            public final String uri;
085    
086            public final String prefix;
087    
088            public final String qName;
089    
090            public final Set<PrefixMapping> mappings = new HashSet<PrefixMapping>();
091    
092            /**
093             * @param uri
094             * @param qName
095             */
096            public StackNode(String uri, String qName, String prefix) {
097                this.uri = uri;
098                this.qName = qName;
099                this.prefix = prefix;
100            }
101        }
102    
103        private final static Map<String, String> WELL_KNOWN_ATTRIBUTE_PREFIXES = new HashMap<String, String>();
104    
105        static {
106            WELL_KNOWN_ATTRIBUTE_PREFIXES.put("adobe:ns:meta/", "x");
107            WELL_KNOWN_ATTRIBUTE_PREFIXES.put(
108                    "http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd",
109                    "sodipodi");
110            WELL_KNOWN_ATTRIBUTE_PREFIXES.put(
111                    "http://ns.adobe.com/AdobeIllustrator/10.0/", "i");
112            WELL_KNOWN_ATTRIBUTE_PREFIXES.put(
113                    "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/", "a");
114            WELL_KNOWN_ATTRIBUTE_PREFIXES.put(
115                    "http://ns.adobe.com/Extensibility/1.0/", "x");
116            WELL_KNOWN_ATTRIBUTE_PREFIXES.put(
117                    "http://ns.adobe.com/illustrator/1.0/", "illustrator");
118            WELL_KNOWN_ATTRIBUTE_PREFIXES.put("http://ns.adobe.com/pdf/1.3/", "pdf");
119            WELL_KNOWN_ATTRIBUTE_PREFIXES.put("http://ns.adobe.com/photoshop/1.0/",
120                    "photoshop");
121            WELL_KNOWN_ATTRIBUTE_PREFIXES.put("http://ns.adobe.com/tiff/1.0/",
122                    "tiff");
123            WELL_KNOWN_ATTRIBUTE_PREFIXES.put("http://ns.adobe.com/xap/1.0/", "xap");
124            WELL_KNOWN_ATTRIBUTE_PREFIXES.put("http://ns.adobe.com/xap/1.0/g/",
125                    "xapG");
126            WELL_KNOWN_ATTRIBUTE_PREFIXES.put("http://ns.adobe.com/xap/1.0/mm/",
127                    "xapMM");
128            WELL_KNOWN_ATTRIBUTE_PREFIXES.put(
129                    "http://ns.adobe.com/xap/1.0/rights/", "xapRights");
130            WELL_KNOWN_ATTRIBUTE_PREFIXES.put(
131                    "http://ns.adobe.com/xap/1.0/sType/Dimensions#", "stDim");
132            WELL_KNOWN_ATTRIBUTE_PREFIXES.put(
133                    "http://ns.adobe.com/xap/1.0/sType/ResourceRef#", "stRef");
134            WELL_KNOWN_ATTRIBUTE_PREFIXES.put("http://ns.adobe.com/xap/1.0/t/pg/",
135                    "xapTPg");
136            WELL_KNOWN_ATTRIBUTE_PREFIXES.put("http://purl.org/dc/elements/1.1/",
137                    "dc");
138            WELL_KNOWN_ATTRIBUTE_PREFIXES.put(
139                    "http://schemas.microsoft.com/visio/2003/SVGExtensions/", "v");
140            WELL_KNOWN_ATTRIBUTE_PREFIXES.put(
141                    "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd",
142                    "sodipodi");
143            WELL_KNOWN_ATTRIBUTE_PREFIXES.put("http://w3.org/1999/xlink", "xlink");
144            WELL_KNOWN_ATTRIBUTE_PREFIXES.put("http://www.carto.net/attrib/",
145                    "attrib");
146            WELL_KNOWN_ATTRIBUTE_PREFIXES.put(
147                    "http://www.iki.fi/pav/software/textext/", "textext");
148            WELL_KNOWN_ATTRIBUTE_PREFIXES.put(
149                    "http://www.inkscape.org/namespaces/inkscape", "inkscape");
150            WELL_KNOWN_ATTRIBUTE_PREFIXES.put(
151                    "http://www.justsystem.co.jp/hanako13/svg", "jsh");
152            WELL_KNOWN_ATTRIBUTE_PREFIXES.put(
153                    "http://www.w3.org/1999/02/22-rdf-syntax-ns#", "rdf");
154            WELL_KNOWN_ATTRIBUTE_PREFIXES.put("http://www.w3.org/1999/xlink",
155                    "xlink");
156            WELL_KNOWN_ATTRIBUTE_PREFIXES.put(
157                    "http://www.w3.org/2001/XMLSchema-instance", "xsi");
158            WELL_KNOWN_ATTRIBUTE_PREFIXES.put("http://www.w3org/1999/xlink",
159                    "xlink");
160        }
161    
162        private final static Map<String, String> WELL_KNOWN_ELEMENT_PREFIXES = new HashMap<String, String>();
163    
164        static {
165            WELL_KNOWN_ELEMENT_PREFIXES.put("http://www.w3.org/1999/XSL/Transform",
166                    "xsl");
167            WELL_KNOWN_ELEMENT_PREFIXES.put(
168                    "http://www.w3.org/1999/02/22-rdf-syntax-ns#", "rdf");
169            WELL_KNOWN_ELEMENT_PREFIXES.put("http://purl.org/dc/elements/1.1/",
170                    "dc");
171            WELL_KNOWN_ELEMENT_PREFIXES.put(
172                    "http://www.w3.org/2001/XMLSchema-instance", "xsi");
173            WELL_KNOWN_ELEMENT_PREFIXES.put("http://www.ascc.net/xml/schematron",
174                    "sch");
175            WELL_KNOWN_ELEMENT_PREFIXES.put("http://purl.oclc.org/dsdl/schematron",
176                    "sch");
177            WELL_KNOWN_ELEMENT_PREFIXES.put(
178                    "http://www.inkscape.org/namespaces/inkscape", "inkscape");
179            WELL_KNOWN_ELEMENT_PREFIXES.put(
180                    "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd",
181                    "sodipodi");
182            WELL_KNOWN_ELEMENT_PREFIXES.put(
183                    "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/", "a");
184            WELL_KNOWN_ELEMENT_PREFIXES.put(
185                    "http://ns.adobe.com/AdobeIllustrator/10.0/", "i");
186            WELL_KNOWN_ELEMENT_PREFIXES.put("adobe:ns:meta/", "x");
187            WELL_KNOWN_ELEMENT_PREFIXES.put("http://ns.adobe.com/xap/1.0/", "xap");
188            WELL_KNOWN_ELEMENT_PREFIXES.put("http://ns.adobe.com/pdf/1.3/", "pdf");
189            WELL_KNOWN_ELEMENT_PREFIXES.put("http://ns.adobe.com/tiff/1.0/", "tiff");
190            WELL_KNOWN_ELEMENT_PREFIXES.put("http://creativecommons.org/ns#", "cc");
191            WELL_KNOWN_ELEMENT_PREFIXES.put(
192                    "http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd",
193                    "sodipodi");
194            WELL_KNOWN_ELEMENT_PREFIXES.put(
195                    "http://iptc.org/std/Iptc4xmpCore/1.0/xmlns/", "Iptc4xmpCore");
196            WELL_KNOWN_ELEMENT_PREFIXES.put("http://ns.adobe.com/exif/1.0/", "exif");
197            WELL_KNOWN_ELEMENT_PREFIXES.put(
198                    "http://ns.adobe.com/Extensibility/1.0/", "x");
199            WELL_KNOWN_ELEMENT_PREFIXES.put("http://ns.adobe.com/illustrator/1.0/",
200                    "illustrator");
201            WELL_KNOWN_ELEMENT_PREFIXES.put("http://ns.adobe.com/pdfx/1.3/", "pdfx");
202            WELL_KNOWN_ELEMENT_PREFIXES.put("http://ns.adobe.com/photoshop/1.0/",
203                    "photoshop");
204            WELL_KNOWN_ELEMENT_PREFIXES.put("http://ns.adobe.com/Variables/1.0/",
205                    "v");
206            WELL_KNOWN_ELEMENT_PREFIXES.put("http://ns.adobe.com/xap/1.0/g/",
207                    "xapG");
208            WELL_KNOWN_ELEMENT_PREFIXES.put("http://ns.adobe.com/xap/1.0/g/img/",
209                    "xapGImg");
210            WELL_KNOWN_ELEMENT_PREFIXES.put("http://ns.adobe.com/xap/1.0/mm/",
211                    "xapMM");
212            WELL_KNOWN_ELEMENT_PREFIXES.put("http://ns.adobe.com/xap/1.0/rights/",
213                    "xapRights");
214            WELL_KNOWN_ELEMENT_PREFIXES.put(
215                    "http://ns.adobe.com/xap/1.0/sType/Dimensions#", "stDim");
216            WELL_KNOWN_ELEMENT_PREFIXES.put(
217                    "http://ns.adobe.com/xap/1.0/sType/Font#", "stFnt");
218            WELL_KNOWN_ELEMENT_PREFIXES.put(
219                    "http://ns.adobe.com/xap/1.0/sType/ResourceRef#", "stRef");
220            WELL_KNOWN_ELEMENT_PREFIXES.put("http://ns.adobe.com/xap/1.0/t/pg/",
221                    "xapTPg");
222            WELL_KNOWN_ELEMENT_PREFIXES.put(
223                    "http://product.corel.com/CGS/11/cddns/", "odm");
224            WELL_KNOWN_ELEMENT_PREFIXES.put(
225                    "http://schemas.microsoft.com/visio/2003/SVGExtensions/", "v");
226            WELL_KNOWN_ELEMENT_PREFIXES.put("http://web.resource.org/cc/", "cc");
227            WELL_KNOWN_ELEMENT_PREFIXES.put(
228                    "http://www.freesoftware.fsf.org/bkchem/cdml", "cdml");
229            WELL_KNOWN_ELEMENT_PREFIXES.put("http://www.opengis.net/gml", "gml");
230            WELL_KNOWN_ELEMENT_PREFIXES.put("http://www.svgmaker.com/svgns",
231                    "svgmaker");
232            WELL_KNOWN_ELEMENT_PREFIXES.put(
233                    "http://www.w3.org/2000/01/rdf-schema#", "rdfs");
234            WELL_KNOWN_ELEMENT_PREFIXES.put("http://xmlns.com/foaf/0.1/", "foaf");
235            WELL_KNOWN_ELEMENT_PREFIXES.put("http://www.xml-cml.org/schema/stmml",
236                    "stm");
237            WELL_KNOWN_ELEMENT_PREFIXES.put("http://www.iupac.org/foo/ichi", "ichi");
238        }
239    
240        private final static Writer wrap(OutputStream out) {
241            Charset charset = Charset.forName("utf-8");
242            CharsetEncoder encoder = charset.newEncoder();
243            encoder.onMalformedInput(CodingErrorAction.REPLACE);
244            encoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
245            try {
246                encoder.replaceWith("\uFFFD".getBytes("utf-8"));
247            } catch (UnsupportedEncodingException e) {
248                throw new RuntimeException(e);
249            }
250            return new OutputStreamWriter(out, encoder);
251        }
252    
253        // grows from head
254        private final LinkedList<StackNode> stack = new LinkedList<StackNode>();
255    
256        private final Writer writer;
257    
258        public XmlSerializer(OutputStream out) {
259            this(wrap(out));
260        }
261    
262        public XmlSerializer(Writer out) {
263            this.writer = out;
264        }
265        
266        protected void checkNCName(String name) throws SAXException {
267            
268        }
269    
270        private final void push(String uri, String local, String prefix) {
271            stack.addFirst(new StackNode(uri, local, prefix));
272        }
273    
274        private final String pop() {
275            String rv = stack.removeFirst().qName;
276            stack.getFirst().mappings.clear();
277            return rv;
278        }
279    
280        private final String lookupPrefixAttribute(String ns) {
281            if ("http://www.w3.org/XML/1998/namespace".equals(ns)) {
282                return "xml";
283            }
284            Set<String> hidden = new HashSet<String>();
285            for (StackNode node : stack) {
286                for (PrefixMapping mapping : node.mappings) {
287                    if (mapping.prefix.length() != 0 && mapping.uri.equals(ns)
288                            && !hidden.contains(mapping.prefix)) {
289                        return mapping.prefix;
290                    }
291                    hidden.add(mapping.prefix);
292                }
293            }
294            return null;
295        }
296    
297        private final String lookupUri(String prefix) {
298            for (StackNode node : stack) {
299                for (PrefixMapping mapping : node.mappings) {
300                    if (mapping.prefix.equals(prefix)) {
301                        return mapping.uri;
302                    }
303                }
304            }
305            return null;
306        }
307    
308        private final boolean xmlNsQname(String name) {
309            if (name == null) {
310                return false;
311            } else if ("xmlns".equals(name)) {
312                return true;
313            } else if (name.startsWith("xmlns:")) {
314                return true;
315            } else {
316                return false;
317            }
318        }
319    
320        private final void writeAttributeValue(String val) throws IOException {
321            boolean prevWasSpace = true;
322            int last = val.length() - 1;
323            for (int i = 0; i <= last; i++) {
324                char c = val.charAt(i);
325                switch (c) {
326                    case '<':
327                        writer.write("&lt;");
328                        prevWasSpace = false;
329                        break;
330                    case '>':
331                        writer.write("&gt;");
332                        prevWasSpace = false;
333                        break;
334                    case '&':
335                        writer.write("&amp;");
336                        prevWasSpace = false;
337                        break;
338                    case '"':
339                        writer.write("&quot;");
340                        prevWasSpace = false;
341                        break;
342                    case '\r':
343                        writer.write("&#xD;");
344                        prevWasSpace = false;
345                        break;
346                    case '\t':
347                        writer.write("&#x9;");
348                        prevWasSpace = false;
349                        break;
350                    case '\n':
351                        writer.write("&#xA;");
352                        prevWasSpace = false;
353                        break;
354                    case ' ':
355                        if (prevWasSpace || i == last) {
356                            writer.write("&#x20;");
357                            prevWasSpace = false;
358                        } else {
359                            writer.write(' ');
360                            prevWasSpace = true;
361                        }
362                        break;
363                    case '\uFFFE':
364                        writer.write('\uFFFD');
365                        prevWasSpace = false;
366                        break;
367                    case '\uFFFF':
368                        writer.write('\uFFFD');
369                        prevWasSpace = false;
370                        break;
371                    default:
372                        if (c < ' ') {
373                            writer.write('\uFFFD');
374                        } else {
375                            writer.write(c);
376                        }
377                        prevWasSpace = false;
378                        break;
379                }
380            }
381        }
382    
383        private final void generatePrefix(String uri) throws SAXException {
384            int counter = 0;
385            String candidate = WELL_KNOWN_ATTRIBUTE_PREFIXES.get(uri);
386            if (candidate == null) {
387                candidate = "p" + (counter++);
388            }
389            while (lookupUri(candidate) != null) {
390                candidate = "p" + (counter++);
391            }
392            startPrefixMappingPrivate(candidate, uri);
393        }
394    
395        public final void characters(char[] ch, int start, int length)
396                throws SAXException {
397            try {
398                for (int i = start; i < start + length; i++) {
399                    char c = ch[i];
400                    switch (c) {
401                        case '<':
402                            writer.write("&lt;");
403                            break;
404                        case '>':
405                            writer.write("&gt;");
406                            break;
407                        case '&':
408                            writer.write("&amp;");
409                            break;
410                        case '\r':
411                            writer.write("&#xD;");
412                            break;
413                        case '\t':
414                            writer.write('\t');
415                            break;
416                        case '\n':
417                            writer.write('\n');
418                            break;
419                        case '\uFFFE':
420                            writer.write('\uFFFD');
421                            break;
422                        case '\uFFFF':
423                            writer.write('\uFFFD');
424                            break;
425                        default:
426                            if (c < ' ') {
427                                writer.write('\uFFFD');
428                            } else {
429                                writer.write(c);
430                            }
431                            break;
432                    }
433                }
434            } catch (IOException e) {
435                throw new SAXException(e);
436            }
437        }
438    
439        public final void endDocument() throws SAXException {
440            try {
441                stack.clear();
442                writer.flush();
443                writer.close();
444            } catch (IOException e) {
445                throw new SAXException(e);
446            }
447        }
448    
449        public final void endElement(String uri, String localName, String qName)
450                throws SAXException {
451            try {
452                writer.write('<');
453                writer.write('/');
454                writer.write(pop());
455                writer.write('>');
456            } catch (IOException e) {
457                throw new SAXException(e);
458            }
459        }
460    
461        public final void ignorableWhitespace(char[] ch, int start, int length)
462                throws SAXException {
463            characters(ch, start, length);
464        }
465    
466        public final void processingInstruction(String target, String data)
467                throws SAXException {
468            try {
469                checkNCName(target);
470                writer.write("<?");
471                writer.write(target);
472                writer.write(' ');
473                boolean prevWasQuestionmark = false;
474                for (int i = 0; i < data.length(); i++) {
475                    char c = data.charAt(i);
476                    switch (c) {
477                        case '?':
478                            writer.write('?');
479                            prevWasQuestionmark = true;
480                            break;
481                        case '>':
482                            if (prevWasQuestionmark) {
483                                writer.write(" >");
484                            } else {
485                                writer.write('>');
486                            }
487                            prevWasQuestionmark = false;
488                            break;
489                        case '\t':
490                            writer.write('\t');
491                            prevWasQuestionmark = false;
492                            break;
493                        case '\r':
494                        case '\n':
495                            writer.write('\n');
496                            prevWasQuestionmark = false;
497                            break;
498                        case '\uFFFE':
499                            writer.write('\uFFFD');
500                            prevWasQuestionmark = false;
501                            break;
502                        case '\uFFFF':
503                            writer.write('\uFFFD');
504                            prevWasQuestionmark = false;
505                            break;
506                        default:
507                            if (c < ' ') {
508                                writer.write('\uFFFD');
509                            } else {
510                                writer.write(c);
511                            }
512                            prevWasQuestionmark = false;
513                            break;
514                    }
515                }
516                writer.write("?>");
517            } catch (IOException e) {
518                throw new SAXException(e);
519            }
520        }
521    
522        public final void setDocumentLocator(Locator locator) {
523        }
524    
525        public final void startDocument() throws SAXException {
526            try {
527                writer.write("<?xml version='1.0' encoding='utf-8'?>\n");
528            } catch (IOException e) {
529                throw new SAXException(e);
530            }
531            stack.clear();
532            push(null, null, null);
533        }
534    
535        public final void startElement(String uri, String localName, String q,
536                Attributes atts) throws SAXException {
537            checkNCName(localName);
538            String prefix;
539            String qName;
540            if (uri.length() == 0) {
541                prefix = "";
542                qName = localName;
543                // generate xmlns
544                startPrefixMappingPrivate(prefix, uri);
545            } else {
546                prefix = WELL_KNOWN_ELEMENT_PREFIXES.get(uri);
547                if (prefix == null) {
548                    prefix = "";
549                }
550                String lookup = lookupUri(prefix);
551                if (lookup != null && !lookup.equals(uri)) {
552                    prefix = "";
553                }
554                startPrefixMappingPrivate(prefix, uri);
555                if (prefix.length() == 0) {
556                    qName = localName;
557                } else {
558                    qName = prefix + ':' + localName;
559                }
560            }
561    
562            int attLen = atts.getLength();
563            for (int i = 0; i < attLen; i++) {
564                String attUri = atts.getURI(i);
565                if (attUri.length() == 0
566                        || "http://www.w3.org/XML/1998/namespace".equals(attUri)
567                        || "http://www.w3.org/2000/xmlns/".equals(attUri)
568                        || atts.getLocalName(i).length() == 0
569                        || xmlNsQname(atts.getQName(i))) {
570                    continue;
571                }
572                if (lookupPrefixAttribute(attUri) == null) {
573                    generatePrefix(attUri);
574                }
575            }
576    
577            try {
578                writer.write('<');
579                writer.write(qName);
580                for (PrefixMapping mapping : stack.getFirst().mappings) {
581                    writer.write(' ');
582                    if (mapping.prefix.length() == 0) {
583                        writer.write("xmlns");
584                    } else {
585                        writer.write("xmlns:");
586                        writer.write(mapping.prefix);
587                    }
588                    writer.write('=');
589                    writer.write('"');
590                    writeAttributeValue(mapping.uri);
591                    writer.write('"');
592                }
593    
594                for (int i = 0; i < attLen; i++) {
595                    String attUri = atts.getURI(i);
596                    if ("http://www.w3.org/XML/1998/namespace".equals(attUri)
597                            || "http://www.w3.org/2000/xmlns/".equals(attUri)
598                            || atts.getLocalName(i).length() == 0
599                            || xmlNsQname(atts.getQName(i))) {
600                        continue;
601                    }
602                    writer.write(' ');
603                    if (attUri.length() != 0) {
604                        writer.write(lookupPrefixAttribute(attUri));
605                        writer.write(':');
606                    }
607                    String attLocal = atts.getLocalName(i);
608                    checkNCName(attLocal);
609                    writer.write(attLocal);
610                    writer.write('=');
611                    writer.write('"');
612                    writeAttributeValue(atts.getValue(i));
613                    writer.write('"');
614                }
615                writer.write('>');
616            } catch (IOException e) {
617                throw new SAXException(e);
618            }
619            push(uri, qName, prefix);
620        }
621    
622        public final void comment(char[] ch, int start, int length) throws SAXException {
623            try {
624                boolean prevWasHyphen = false;
625                writer.write("<!--");
626                for (int i = start; i < start + length; i++) {
627                    char c = ch[i];
628                    switch (c) {
629                        case '-':
630                            if (prevWasHyphen) {
631                                writer.write(" -");
632                            } else {
633                                writer.write('-');
634                                prevWasHyphen = true;
635                            }
636                            break;
637                        case '\t':
638                            writer.write('\t');
639                            prevWasHyphen = false;
640                            break;
641                        case '\r':
642                        case '\n':
643                            writer.write('\n');
644                            prevWasHyphen = false;
645                            break;
646                        case '\uFFFE':
647                            writer.write('\uFFFD');
648                            prevWasHyphen = false;
649                            break;
650                        case '\uFFFF':
651                            writer.write('\uFFFD');
652                            prevWasHyphen = false;
653                            break;
654                        default:
655                            if (c < ' ') {
656                                writer.write('\uFFFD');
657                            } else {
658                                writer.write(c);
659                            }
660                            prevWasHyphen = false;
661                            break;
662                    }
663                }
664                if (prevWasHyphen) {
665                    writer.write(' ');
666                }
667                writer.write("-->");
668            } catch (IOException e) {
669                throw new SAXException(e);
670            }
671        }
672    
673        public final void endCDATA() throws SAXException {
674        }
675    
676        public final void endDTD() throws SAXException {
677        }
678    
679        public final void endEntity(String name) throws SAXException {
680        }
681    
682        public final void startCDATA() throws SAXException {
683        }
684    
685        public final void startDTD(String name, String publicId, String systemId)
686                throws SAXException {
687        }
688    
689        public final void startEntity(String name) throws SAXException {
690        }
691    
692        public final void startPrefixMapping(String prefix, String uri)
693                throws SAXException {
694            if (prefix.length() == 0 || uri.equals(lookupUri(prefix))) {
695                return;
696            }
697            if (uri.equals(lookupUri(prefix))) {
698                return;
699            }
700            if ("http://www.w3.org/XML/1998/namespace".equals(uri)) {
701                if ("xml".equals(prefix)) {
702                    return;
703                } else {
704                    throw new SAXException("Attempt to declare a reserved NS uri.");
705                }
706            }
707            if ("http://www.w3.org/2000/xmlns/".equals(uri)) {
708                throw new SAXException("Attempt to declare a reserved NS uri.");
709            }
710            if (uri.length() == 0 && prefix.length() != 0) {
711                throw new SAXException("Can bind a prefix to no namespace.");           
712            }
713            checkNCName(prefix);
714            Set<PrefixMapping> theSet = stack.getFirst().mappings;
715            PrefixMapping mapping = new PrefixMapping(uri, prefix);
716            if (theSet.contains(mapping)) {
717                throw new SAXException(
718                        "Attempt to map one prefix to two URIs on one element.");
719            }
720            theSet.add(mapping);
721        }
722    
723        public final void startPrefixMappingPrivate(String prefix, String uri)
724                throws SAXException {
725            if (uri.equals(lookupUri(prefix))) {
726                return;
727            }
728            stack.getFirst().mappings.add(new PrefixMapping(uri, prefix));
729        }
730    
731        public final void endPrefixMapping(String prefix) throws SAXException {
732        }
733    
734        public final void skippedEntity(String name) throws SAXException {
735        }
736    
737    }