001/*
002 * ModeShape (http://www.modeshape.org)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *       http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.modeshape.common.xml;
017
018import java.io.IOException;
019import java.io.OutputStream;
020import java.io.OutputStreamWriter;
021import java.io.UnsupportedEncodingException;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.Map;
026import org.modeshape.common.SystemFailureException;
027import org.modeshape.common.text.TextEncoder;
028import org.modeshape.common.text.XmlValueEncoder;
029import org.xml.sax.Attributes;
030import org.xml.sax.ContentHandler;
031import org.xml.sax.SAXException;
032import org.xml.sax.helpers.DefaultHandler;
033
034/**
035 * Class that adapts an arbitrary, open {@link OutputStream} to the {@link ContentHandler} interface. SAX events invoked on this
036 * object will be translated into their corresponding XML text and written to the output stream.
037 */
038public class StreamingContentHandler extends DefaultHandler {
039
040    public static final String DEFAULT_ENCODING = "UTF-8";
041
042    /**
043     * Encoder to properly escape XML attribute values
044     * 
045     * @see XmlValueEncoder
046     */
047    private final TextEncoder VALUE_ENCODER = new XmlValueEncoder();
048
049    /**
050     * The list of XML namespaces that are predefined and should not be exported by the content handler.
051     */
052    private final Collection<String> unexportableNamespaces;
053
054    /**
055     * The output stream to which the XML will be written
056     */
057    private final OutputStreamWriter writer;
058
059    /**
060     * The XML namespace prefixes that are currently mapped
061     */
062    private final Map<String, String> mappedPrefixes;
063
064    /**
065     * The XML declaration information written to the output.
066     */
067    private final String declaration;
068
069    public StreamingContentHandler( OutputStream os ) {
070        this(os, Collections.<String>emptyList());
071    }
072
073    public StreamingContentHandler( OutputStream os,
074                                    Collection<String> unexportableNamespaces ) {
075        try {
076            this.writer = new OutputStreamWriter(os, DEFAULT_ENCODING);
077        } catch (UnsupportedEncodingException e) {
078            // This should never happen ...
079            throw new SystemFailureException(e);
080        }
081        this.unexportableNamespaces = unexportableNamespaces;
082        this.mappedPrefixes = new HashMap<String, String>();
083        this.declaration = "version=\"1.0\" encoding=\"" + DEFAULT_ENCODING + "\"";
084    }
085
086    public StreamingContentHandler( OutputStream os,
087                                    Collection<String> unexportableNamespaces,
088                                    String encoding ) throws UnsupportedEncodingException {
089        this.writer = new OutputStreamWriter(os, encoding);
090        this.unexportableNamespaces = unexportableNamespaces;
091        this.mappedPrefixes = new HashMap<String, String>();
092        if (encoding == null || encoding.length() == 0) encoding = "UTF-8";
093        this.declaration = "version=\"1.0\" encoding=\"" + encoding + "\"";
094    }
095
096    @Override
097    public void characters( char[] ch,
098                            int start,
099                            int length ) throws SAXException {
100        emit(VALUE_ENCODER.encode(new String(ch, start, length)));
101    }
102
103    @Override
104    public void startDocument() throws SAXException {
105        emit("<?xml " + declaration + "?>");
106    }
107
108    @Override
109    public void endDocument() throws SAXException {
110        try {
111            writer.flush();
112        } catch (IOException e) {
113            throw new SAXException(e);
114        }
115    }
116
117    @Override
118    public void startElement( String uri,
119                              String localName,
120                              String name,
121                              Attributes attributes ) throws SAXException {
122        emit("<");
123        emit(name);
124
125        for (Map.Entry<String, String> mapping : mappedPrefixes.entrySet()) {
126            emit(" xmlns:");
127            emit(mapping.getKey());
128            emit("=\"");
129            emit(mapping.getValue());
130            emit("\"");
131        }
132
133        mappedPrefixes.clear();
134
135        if (attributes != null) {
136            for (int i = 0; i < attributes.getLength(); i++) {
137                emit(" ");
138                emit(attributes.getQName(i));
139                emit("=\"");
140                emit(VALUE_ENCODER.encode(attributes.getValue(i)));
141                emit("\"");
142            }
143        }
144
145        emit(">");
146    }
147
148    @Override
149    public void endElement( String uri,
150                            String localName,
151                            String name ) throws SAXException {
152        emit("</");
153        emit(name);
154        emit(">");
155        // System.out.println();
156
157    }
158
159    @Override
160    public void startPrefixMapping( String prefix,
161                                    String uri ) {
162        if (!unexportableNamespaces.contains(prefix)) {
163            mappedPrefixes.put(prefix, uri);
164        }
165    }
166
167    /**
168     * Writes the given text to the output stream for this {@link StreamingContentHandler}.
169     * 
170     * @param text the text to output
171     * @throws SAXException if there is an error writing to the stream
172     */
173    private void emit( String text ) throws SAXException {
174
175        try {
176            // System.out.print(text);
177            writer.write(text);
178        } catch (IOException ioe) {
179            throw new SAXException(ioe);
180        }
181    }
182}