001/*
002 * Copyright (c) 2004-2012, Willem Cazander
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification, are permitted provided
006 * that the following conditions are met:
007 * 
008 * * Redistributions of source code must retain the above copyright notice, this list of conditions and the
009 *   following disclaimer.
010 * * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
011 *   the following disclaimer in the documentation and/or other materials provided with the distribution.
012 * 
013 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
014 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
015 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
016 * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
017 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
018 * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
019 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
020 * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
021 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
022 */
023
024package org.x4o.xml.sax;
025
026import java.io.IOException;
027import java.io.OutputStream;
028import java.io.OutputStreamWriter;
029import java.io.UnsupportedEncodingException;
030import java.io.Writer;
031import java.util.ArrayList;
032import java.util.HashMap;
033import java.util.List;
034import java.util.Map;
035import java.util.Set;
036
037import org.xml.sax.Attributes;
038import org.xml.sax.Locator;
039import org.xml.sax.SAXException;
040import org.xml.sax.ext.DefaultHandler2;
041
042
043/**
044 * Writes SAX event to XML.
045 * 
046 * @author Willem Cazander
047 * @version 1.0 17/04/2005
048 */
049public class XMLWriter extends DefaultHandler2 {
050
051        private Writer out = null;
052        private int indent = 0;
053        private Map<String,String> prefixMapping = new HashMap<String,String>(10);
054        private List<String> printedMappings = new ArrayList<String>(10);
055        private StringBuffer startElement = null;
056        private boolean printedReturn = false;
057
058        public XMLWriter(Writer out) {
059                this.out = out;
060        }
061
062        public XMLWriter(OutputStream out) throws UnsupportedEncodingException {
063                this.out = new OutputStreamWriter(out, "UTF-8");
064        }
065        
066        /**
067         * @see org.xml.sax.ContentHandler#startDocument()
068         */
069        @Override
070        public void startDocument() throws SAXException {
071                indent = 0;
072                try {
073                        out.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
074                } catch (IOException e) {
075                        throw new SAXException(e);
076                }
077        }
078        
079        /**
080         * @see org.xml.sax.ContentHandler#endDocument()
081         */
082        @Override
083        public void endDocument() throws SAXException {
084                try {
085                        out.flush();  
086                } catch (IOException e) {
087                        throw new SAXException(e);
088                }
089        }
090
091        /**
092         * @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
093         */
094        @Override
095        public void startElement(String uri, String localName, String name,Attributes atts) throws SAXException {
096                try {
097                        if (startElement!=null) {
098                                out.write(startElement.toString());
099                                startElement=null;
100                        }
101                        
102                        startElement = new StringBuffer(200);
103                        
104                        if (printedReturn==false) {
105                                startElement.append("\r\n");
106                        }
107                        printedReturn=false;
108                        
109                        for (int i = 0; i < indent; i++) {
110                                startElement.append('\t');
111                        }
112                        startElement.append("<");
113                        
114                        if (localName==null) {
115                                localName = "null";
116                        }
117                        
118                        if ("".equals(uri) | uri==null) {
119                                startElement.append(localName);
120                        } else {
121                                String prefix = prefixMapping.get(uri);
122                                if (prefix==null) {
123                                        throw new SAXException("preFixUri: "+uri+" is not started.");
124                                }
125                                if ("".equals(prefix)==false) {
126                                        startElement.append(prefix);
127                                        startElement.append(':');
128                                }
129                                startElement.append(localName);
130                        }
131                
132
133                        if ((uri!=null & "".equals(uri)==false) && printedMappings.contains(uri)==false) {
134                                String prefix = prefixMapping.get(uri);
135                                if (prefix==null) {
136                                        throw new SAXException("preFixUri: "+uri+" is not started.");
137                                }
138                                printedMappings.add(uri);
139                                
140                                startElement.append(' ');
141                                startElement.append("xmlns");
142                                if ("".equals(prefix)==false) {
143                                        startElement.append(':');
144                                        startElement.append(prefix);
145                                }
146                                startElement.append("=\"");
147                                startElement.append(uri);
148                                startElement.append('"');
149                                
150                                boolean first = true;
151                                for (String uri2:prefixMapping.keySet()) {
152                                        if (printedMappings.contains(uri2)==false) {
153                                                prefix = prefixMapping.get(uri2);
154                                                if (prefix==null) {
155                                                        throw new SAXException("preFixUri: "+uri+" is not started.");
156                                                }
157                                                printedMappings.add(uri2);
158                                                
159                                                if (first) {
160                                                        startElement.append('\n');
161                                                        first = false;
162                                                }
163                                                
164                                                startElement.append(' ');
165                                                startElement.append("xmlns");
166                                                if ("".equals(prefix)==false) {
167                                                        startElement.append(':');
168                                                        startElement.append(prefix);
169                                                }
170                                                startElement.append("=\"");
171                                                startElement.append(uri2);
172                                                startElement.append('"');
173                                                startElement.append('\n');
174                                        }
175                                }
176                        }
177                        
178                        for (int i=0;i<atts.getLength();i++) {
179                                String attributeUri = atts.getURI(i);
180                                String attributeName = atts.getLocalName(i);
181                                String attributeValue = atts.getValue(i);
182                                if (attributeValue==null) {
183                                        attributeValue = "null";
184                                }
185                                startElement.append(' ');
186                                if ("".equals(attributeUri) | attributeUri ==null) {
187                                        startElement.append(attributeName);                                     
188                                } else {
189                                        startElement.append(attributeUri);
190                                        startElement.append(':');
191                                        startElement.append(attributeName);
192                                }
193                                
194                                startElement.append("=\"");
195                                
196                                // TODO: xml escaping of attributes
197                                if (attributeValue.contains("&")) {
198                                        attributeValue=attributeValue.replaceAll("&", "&amp;");
199                                }
200                                if (attributeValue.contains("\"")) {
201                                        attributeValue=attributeValue.replaceAll("\"", "&quote;");
202                                }
203                                if (attributeValue.contains("<")) {
204                                        attributeValue=attributeValue.replaceAll("<", "&lt;");
205                                }
206                                if (attributeValue.contains(">")) {
207                                        attributeValue=attributeValue.replaceAll(">", "&gt;");
208                                }
209                                
210                                startElement.append(attributeValue);
211                                startElement.append('"');
212                        }
213                        startElement.append(">");
214                } catch (IOException e) {
215                        throw new SAXException(e);
216                } finally {
217                        indent++;
218                }
219        }
220        
221        /**
222         * @see org.xml.sax.ContentHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
223         */
224        @Override
225        public void endElement(String uri, String localName, String name) throws SAXException {
226                try {
227                        if (startElement!=null) {
228                                String ss = startElement.toString();
229                                out.write(ss,0,ss.length()-1);
230                                out.write("/>");
231                                startElement=null;
232                                indent--;
233                                return;
234                        }
235                        
236                        if (printedReturn==false) {
237                                out.write("\r\n");
238                        }
239                        printedReturn=false;
240                        indent--;
241                        indent();
242                        
243                        if (localName==null) {
244                                localName = "null";
245                        }
246                        
247                        out.write("</");
248                        if ("".equals(uri) | uri==null) {
249                                out.write(localName);
250                        } else {
251                                String prefix = prefixMapping.get(uri);
252                                if (prefix==null) {
253                                        throw new SAXException("preFixUri: "+uri+" is not started.");
254                                }
255                                if ("".equals(prefix)==false) {
256                                        out.write(prefix);
257                                        out.write(':');
258                                }
259                                out.write(localName);
260                        }
261                        out.write(">");   
262                } catch (IOException e) {
263                        throw new SAXException(e);
264                } 
265        }
266                
267        /**
268         * @see org.xml.sax.ContentHandler#startPrefixMapping(java.lang.String, java.lang.String)
269         */
270        @Override
271        public void startPrefixMapping(String prefix, String uri) throws SAXException {
272                prefixMapping.put(uri, prefix);
273        }
274        
275        /**
276         * @see org.xml.sax.ContentHandler#endPrefixMapping(java.lang.String)
277         */
278        @Override
279        public void endPrefixMapping(String prefix) throws SAXException {
280                Set<Map.Entry<String,String>> s=prefixMapping.entrySet();
281                String uri = null;
282                for (Map.Entry<String,String> e:s) {
283                        if (e.getValue()==null) {
284                                continue; // way ?
285                        }
286                        if (e.getValue().equals(prefix)) {
287                                uri = e.getKey();
288                        }
289                }
290                if (uri!=null) {
291                        printedMappings.remove(uri);
292                        prefixMapping.remove(uri);
293                }
294        }
295
296        /**
297         * @see org.xml.sax.ContentHandler#characters(char[], int, int)
298         */
299        @Override
300        public void characters(char[] ch, int start, int length) throws SAXException {
301                try {
302                        if (startElement!=null) {
303                                out.write(startElement.toString());
304                                startElement=null;
305                        }
306                        /// mmm todo improve a bit
307                        for (int i=start;i<(start+length);i++) {
308                                char c = ch[i];
309                                out.write(c);
310                                if (c=='\n') {
311                                        printedReturn=true;
312                                        continue;
313                                }
314                        }
315                        //out.write(ch, start, length); 
316                } catch (IOException e) {
317                        throw new SAXException(e);
318                }
319        }
320        
321        /**
322         * @see org.xml.sax.ContentHandler#ignorableWhitespace(char[], int, int)
323         */
324        @Override
325        public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
326                try {
327                        if (startElement!=null) {
328                                out.write(startElement.toString());
329                                startElement=null;
330                        }
331                        out.write(ch, start, length); 
332                } catch (IOException e) {
333                        throw new SAXException(e);
334                }
335        }
336
337        /**
338         * @see org.xml.sax.ContentHandler#processingInstruction(java.lang.String, java.lang.String)
339         */
340        @Override
341        public void processingInstruction(String target, String data) throws SAXException {
342                try {
343                        indent();
344                        out.write("<?" + target + " " + data + "?>\r\n"); 
345                } catch (IOException e) {
346                        throw new SAXException(e);
347                } 
348        }
349
350        /**
351         * @see org.xml.sax.ContentHandler#setDocumentLocator(org.xml.sax.Locator)
352         */
353        @Override
354        public void setDocumentLocator(Locator locator) {
355        }
356
357        /**
358         * @see org.xml.sax.ContentHandler#skippedEntity(java.lang.String)
359         */
360        @Override
361        public void skippedEntity(String name) throws SAXException {
362                // is for validating parser support, so not needed in xml writing.
363        }
364
365        /**
366         * @see org.xml.sax.ext.DefaultHandler2#comment(char[], int, int)
367         */
368        @Override
369        public void comment(char[] ch, int start, int length) throws SAXException {
370                try {
371                        indent();
372                        out.write("<!--");
373                                        
374                        /// mmm todo improve a bit
375                        for (int i=start;i<(start+length);i++) {
376                                char c = ch[i];
377                                if (c=='\n') {
378                                        out.write(c);
379                                        indent();
380                                        continue;
381                                }
382                                out.write(c);
383                        }
384                        out.write("-->");
385                } catch (IOException e) {
386                        throw new SAXException(e);
387                }
388        }
389
390        private void indent() throws IOException {
391                for (int i = 0; i < indent; i++) {
392                        out.write('\t');
393                }
394        }
395}