001/*
002Copyright (c) 2011+, HL7, Inc
003All rights reserved.
004
005Redistribution and use in source and binary forms, with or without modification, 
006are permitted provided that the following conditions are met:
007
008 * Redistributions of source code must retain the above copyright notice, this 
009   list of conditions and the following disclaimer.
010 * Redistributions in binary form must reproduce the above copyright notice, 
011   this list of conditions and the following disclaimer in the documentation 
012   and/or other materials provided with the distribution.
013 * Neither the name of HL7 nor the names of its contributors may be used to 
014   endorse or promote products derived from this software without specific 
015   prior written permission.
016
017THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
018ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
019WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
020IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
021INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
022NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
023PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
024WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
025ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
026POSSIBILITY OF SUCH DAMAGE.
027
028*/
029
030package org.hl7.fhir.utilities.xml;
031
032import java.io.IOException;
033import java.io.OutputStream;
034import java.io.OutputStreamWriter;
035import java.io.UnsupportedEncodingException;
036
037/**
038 * XML Writer class.
039 */
040public class XMLWriter extends OutputStreamWriter implements IXMLWriter {
041
042        private boolean xmlHeader = true;
043        private String charset;
044        private boolean prettyBase;
045        private boolean prettyHeader;
046        private boolean pendingClose;
047        private boolean pendingOpen;
048        private String pendingComment;
049        private int lineType = LINE_UNIX;
050        private OutputStream stream;
051        private boolean started = false;
052        private String[] specialAttributeNames = new String[] {"id", "name" };
053        private boolean sortAttributes;
054        private int attributeLineWrap;
055        
056        public final static int LINE_UNIX = 0;
057        public final static int LINE_WINDOWS = 1;
058
059        public XMLWriter(OutputStream stream, String charset) throws UnsupportedEncodingException {
060                super(stream, charset);
061                this.stream = stream;
062                this.charset = charset;
063        }
064
065        protected boolean condition(boolean bTest, String message) throws IOException {
066                if (!bTest)
067                        throw new IOException(message);
068                return bTest;
069        }
070
071        // -- writing context ------------------------------------------------
072
073
074        
075        /**
076         * Returns the encoding.
077         * 
078         * @param charset
079         * @return encoding
080         * @throws IOException
081         */
082        public static String getXMLCharsetName(String charset) throws IOException {
083                if (charset == null || charset.equals(""))
084                        return "UTF-8";
085                else if (charset.equals("US-ASCII"))
086                        return "UTF-8";
087                else if (XMLUtil.charSetImpliesAscii(charset))
088                        return "ISO-8859-1";
089                else if (charset.equals("UTF-8"))
090                        return "UTF-8";
091                else if (charset.equals("UTF-16") || charset.equals("UTF-16BE") || charset.equals("UTF-16LE"))
092                        return "UTF-16";
093                else 
094                        throw new IOException("Unknown charset encoding "+charset);
095        }
096
097        /* (non-Javadoc)
098         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#start()
099         */
100        @Override
101        public void start() throws IOException {
102                condition(!started, "attempt to start after starting");
103                levels.clear();
104                attributes = null;
105                try {
106                        if (xmlHeader) {
107                                write("<?xml version=\"1.0\" encoding=\""+getXMLCharsetName(charset)+"\"?>");
108                                if (prettyBase || prettyHeader)
109                                        write(lineType == LINE_UNIX ? "\n" : "\r\n");
110                        }
111                } catch (UnsupportedEncodingException e) {
112                        // TODO Auto-generated catch block
113                        throw new IOException(e.getMessage());
114                }
115                started = true;
116        }
117
118        private void checkStarted () throws IOException {
119                condition(started, "not started");
120        }
121
122        private void checkInElement() throws IOException {
123                condition(levels.size() > 0, "not in an element");
124        }
125
126        // -- attributes ----------------------------------------------------
127
128        private String[][] attributes;
129        
130        private void addAttribute(String name, String value) throws IOException {
131                addAttribute(name, value, false);
132        }
133
134        private void addAttribute(String name, String value, boolean noLines) throws IOException {
135                if (!XMLUtil.isNMToken(name))
136                        throw new IOException("XML name "+name+" is not valid for value '"+value+"'");
137
138                newLevelIfRequired();
139                value = XMLUtil.escapeXML(value, charset, noLines);
140
141                if (attributes == null) 
142                        attributes = new String[][] {{name, value}};
143                else {
144                        String[][] newattr = new String[attributes.length+1][];
145                        for (int i = 0; i < attributes.length; i++) {
146                                condition(!attributes[i][0].equals(name), "attempt to define attribute with name "+name+" more than once for value '"+value+"'");
147                                newattr[i] = attributes[i];
148                        }
149                        attributes = newattr;
150                        attributes[attributes.length-1] = new String[] {name, value};
151                }
152        }
153
154        protected String getAttribute(String name) {
155                if (attributes != null) {
156                        for (int i = 0; i < attributes.length; i++) {
157                                if (attributes[i][0].equals(name)) {
158                                        return attributes[i][1];
159                                }
160                        }                       
161                }
162                return null;
163        }
164        
165        protected void setAttribute(String name, String value) throws IOException {
166                newLevelIfRequired();
167                if (attributes == null) 
168                        addAttribute(name, value, false);
169                else {
170                        for (int i = 0; i < attributes.length; i++) {
171                                if (attributes[i][0].equals(name)) {
172                                        attributes[i][1] = XMLUtil.escapeXML(value, charset, false);
173                                        return;
174                                }
175                        }
176                        addAttribute(name, value);
177                }
178        }
179
180        protected void commitAttributes() throws IOException {
181                
182        }
183
184        
185        private boolean nameIsSpecial(String name) {
186                for (int i = 0; i < specialAttributeNames.length; i++) {
187                        String n = specialAttributeNames[i];
188                        if (n.equalsIgnoreCase(name))
189                                return true;
190                }
191                return false;
192        }
193        
194        private void writeAttributes(int col) throws IOException {
195                commitAttributes();
196                if (attributes != null && sortAttributes)
197                        sortAttributes();       
198                int c = col;
199                c = writeAttributeSet(true, c, col);
200                writeAttributeSet(false, c, col);
201                attributes = null;
202        }
203
204
205        private void sortAttributes() {
206                // bubble sort - look, it's easy
207                for (int i = 0; i < attributes.length - 1; i++) {
208                        for (int j = 0; j < attributes.length - 1; j++) {
209                                if (String.CASE_INSENSITIVE_ORDER.compare(attributes[j][0], attributes[j+1][0]) < 0) {
210                                        String[] t = attributes[j];
211                                        attributes[j] = attributes[j+1];
212                                        attributes[j+1] = t;
213                                }
214                        }
215                }
216                
217        }
218
219
220        private int writeAttributeSet(boolean special, int col, int wrap) throws IOException {
221                // first pass: name, id
222                if (attributes != null) {
223                        for (int i=0; i < attributes.length; i++) {
224                                String[] element = attributes[i];
225                                if (nameIsSpecial(element[0]) == special) {
226                                        col = col + element[0].length()+element[1].length() + 4;
227                                        if (isPretty() && attributeLineWrap > 0 && col > attributeLineWrap && col > wrap) {
228                                                write(lineType == LINE_UNIX ? "\n" : "\r\n");
229                                                for (int j = 0; j < wrap; j++)
230                                                        write(" ");
231                                                col = wrap;
232                                        }
233                                        write(' ');
234                                        write(element[0]);
235                                        write("=\"");
236                                        if (element[1] != null)
237                                                write(xmlEscape(element[1]));
238                                        write("\"");
239                                }
240                        }
241                }
242                return col;
243        }
244
245         protected String xmlEscape(String s) {
246     StringBuilder b = new StringBuilder();
247     for (char c : s.toCharArray()) {
248       if (c < ' ' || c > '~') {
249         b.append("&#x");
250         b.append(Integer.toHexString(c).toUpperCase());
251         b.append(";");
252       } else
253         b.append(c);
254     }
255     return b.toString();
256   }
257        /* (non-Javadoc)
258         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#attribute(java.lang.String, java.lang.String, java.lang.String, boolean)
259         */
260        @Override
261        public void attribute(String namespace, String name, String value, boolean onlyIfNotEmpty) throws IOException {
262                if (!onlyIfNotEmpty || value != null && !value.equals(""))
263                        attribute(namespace, name, value);
264        }
265        
266        /* (non-Javadoc)
267         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#attribute(java.lang.String, java.lang.String, java.lang.String)
268         */
269        @Override
270        public void attribute(String namespace, String name, String value) throws IOException {
271
272                checkStarted();
273                if (namespace == null || namespace.equals("")) 
274                        addAttribute(name, value);
275                else
276                        addAttribute(getNSAbbreviation(namespace)+name, value);
277        }
278
279        /* (non-Javadoc)
280         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#attribute(java.lang.String, java.lang.String, boolean)
281         */
282        @Override
283        public void attribute(String name, String value, boolean onlyIfNotEmpty) throws IOException {
284                if (!onlyIfNotEmpty || value != null && !value.equals(""))
285                        attribute(name, value);
286        }
287
288        /* (non-Javadoc)
289         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#attribute(java.lang.String, java.lang.String)
290         */
291        @Override
292        public void attribute(String name, String value) throws IOException {
293                checkStarted();
294                addAttribute(name, value);
295        }
296
297        /* (non-Javadoc)
298         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#attributeNoLines(java.lang.String, java.lang.String)
299         */
300        @Override
301        public void attributeNoLines(String name, String value) throws IOException {
302                checkStarted();
303                addAttribute(name, value, true);
304        }
305
306        // -- levels -------------------------------------------------
307
308        private XMLWriterStateStack levels = new XMLWriterStateStack();
309
310        private void newLevelIfRequired() throws IOException {
311                if (!pendingOpen) {
312                        if (!levels.empty())
313                                levels.current().seeChild();
314                        XMLWriterState level = new XMLWriterState();
315                        level.setPretty(isPretty());
316                        levels.push(level);
317                        pendingOpen = true;
318                }
319        }
320
321        // -- namespaces ---------------------------------------------
322
323
324        private void defineNamespace(String namespace, String abbrev) throws IOException {
325                checkStarted();
326                if (namespace != null && !namespace.equals("")) {
327                        if ("".equals(abbrev))
328                                abbrev = null;
329
330                        newLevelIfRequired();
331
332                        levels.current().addNamespaceDefn(namespace, abbrev);
333                        if (abbrev == null)
334                                addAttribute("xmlns", namespace);
335                        else
336                                addAttribute("xmlns:"+abbrev, namespace);
337                }
338        }
339
340        /* (non-Javadoc)
341         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#findByNamespace(java.lang.String)
342         */
343        public XMLNamespace findByNamespace(String namespace) {
344                for (int i = levels.size() - 1; i >= 0; i--) {
345                        XMLNamespace ns = levels.item(i).getDefnByNamespace(namespace);
346                        if (ns != null)
347                                return ns;
348                }
349                return null;
350        }
351
352        /* (non-Javadoc)
353         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#namespaceDefined(java.lang.String)
354         */
355        @Override
356        public boolean namespaceDefined(String namespace) {
357                return namespace == null || namespace.equals("") || findByNamespace(namespace) != null;
358        }
359
360        /* (non-Javadoc)
361         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#findByAbbreviation(java.lang.String)
362         */
363        public XMLNamespace findByAbbreviation(String abbreviation) {
364                for (int i = levels.size() - 1; i >= 0; i--) {
365                        XMLNamespace ns = levels.item(i).getDefnByAbbreviation(abbreviation);
366                        if (ns != null)
367                                return ns;
368                }
369                return null;
370        }
371
372        /* (non-Javadoc)
373         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#abbreviationDefined(java.lang.String)
374         */
375        @Override
376        public boolean abbreviationDefined(String abbreviation) {
377                return findByAbbreviation(abbreviation) != null;
378        }
379
380        protected XMLNamespace findDefaultNamespace() {
381                for (int i = levels.size() - 1; i >= 0; i--) {
382                        XMLNamespace ns = levels.item(i).getDefaultNamespace();
383                        if (ns != null)
384                                return ns;
385                }
386                return null;
387        }
388
389        /* (non-Javadoc)
390         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#getDefaultNamespace()
391         */
392        @Override
393        public String getDefaultNamespace() {
394                XMLNamespace ns = findDefaultNamespace();
395                if (ns == null)
396                        return null;
397                else
398                        return ns.getNamespace();
399        }
400
401        /* (non-Javadoc)
402         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#namespace(java.lang.String)
403         */
404        @Override
405        public void namespace(String namespace) throws IOException {
406                if (!namespaceDefined(namespace)) {
407                        int index = 0;
408                        while (abbreviationDefined("ns"+Integer.toString(index))) 
409                                index++;
410                        defineNamespace(namespace, "ns"+Integer.toString(index));
411                }
412        }
413
414        /* (non-Javadoc)
415         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#defaultNamespace(java.lang.String)
416         * 
417         * Replace defaultNamespace()
418         */
419        @Override
420        public void setDefaultNamespace(String namespace) throws IOException {
421                if ((namespace == null && getDefaultNamespace() != null) ||
422                                (namespace != null && !namespace.equals(getDefaultNamespace())))
423                        defineNamespace(namespace, "");                 
424        }
425
426        /* (non-Javadoc)
427         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#namespace(java.lang.String, java.lang.String)
428         */
429        @Override
430        public void namespace(String namespace, String abbreviation) throws IOException {
431                XMLNamespace ns = findByAbbreviation(abbreviation);
432                if (ns == null || !ns.getNamespace().equals(namespace))
433                        defineNamespace(namespace, abbreviation);
434        }
435
436
437        private String getNSAbbreviation(String namespace) throws IOException {
438                if ("http://www.w3.org/XML/1998/namespace".equals(namespace))
439                        return "xml:";
440                
441                if (namespace == null || "".equals(namespace))
442                        return "";
443                
444                XMLNamespace ns = findByNamespace(namespace);
445                if (ns == null)
446                        throw new IOException("Namespace "+namespace+" is not defined");
447                else if (ns.getAbbreviation() == null)
448                        return "";
449                else
450                        return ns.getAbbreviation()+":";
451        }
452
453        // -- public API -----------------------------------------------------------
454
455        /* (non-Javadoc)
456         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#comment(java.lang.String, boolean)
457         */
458        @Override
459        public void comment(String comment, boolean doPretty) throws IOException {
460                checkStarted();
461                if (pendingClose) { 
462                        write('>');
463                        writePendingComment();
464                        pendingClose = false;
465                }
466                if (doPretty) {
467                        writePretty();
468                }
469                if (levels.inComment())
470                        write("<!-- "+comment+" -- >");
471                else
472                        write("<!-- "+comment+" -->");
473                if (doPretty && !isPretty())
474                        writePretty();
475        }
476
477
478        private void writePendingComment() throws IOException {
479                if (pendingComment != null) {
480                        if (isPretty())
481                                write("   ");
482                        if (levels.inComment())
483                                write("<!-- "+pendingComment+" -- >");
484                        else
485                                write("<!-- "+pendingComment+" -->");
486                }
487        }
488        
489        /* (non-Javadoc)
490         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#open(java.lang.String, java.lang.String)
491         */
492        @Override
493        public void enter(String namespace, String name) throws IOException {
494                enter(namespace, name, null);
495        }
496
497
498        /* (non-Javadoc)
499         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#open(java.lang.String, java.lang.String, java.lang.String)
500         */
501        @Override
502        public void enter(String namespace, String name, String comment) throws IOException {
503                if (name == null)
504                        throw new Error("name == null");
505                if (!XMLUtil.isNMToken(name))
506                        throw new IOException("XML name "+name+" is not valid");
507                checkStarted();
508                if (pendingClose) { 
509                        write('>');
510                        writePendingComment();
511                        pendingClose = false;
512                }
513
514                if (name == null) {
515                        throw new IOException("name is null");
516                }
517                newLevelIfRequired();
518                levels.current().setName(name);
519                levels.current().setNamespace(namespace);
520                int col = writePretty();
521                write('<');
522                if (namespace == null) {
523                        write(name);
524                        col = col + name.length()+1;
525                } else {
526                        String n = getNSAbbreviation(namespace)+name;
527                        write(n);
528                        col = col + n.length()+1;
529                }
530                writeAttributes(col);
531                pendingOpen = false;
532                pendingClose = true;
533                pendingComment = comment;
534        }
535
536
537        /* (non-Javadoc)
538         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#close(java.lang.String)
539         */
540        @Override
541        public void exit(String name) throws IOException {
542                checkStarted();
543                if (levels.empty())
544                        throw new IOException("Unable to close null|"+name+", nothing to close");
545                if (levels.current().getNamespace() != null || !levels.current().getName().equals(name))
546                        throw new IOException("Unable to close null|"+name+", found "+levels.current().getNamespace()+"|"+levels.current().getName());
547                exit();
548        }
549
550        /* (non-Javadoc)
551         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#close(java.lang.String, java.lang.String)
552         */
553        @Override
554        public void exit(String namespace, String name) throws IOException {
555                checkStarted();
556                if (levels.empty())
557                        throw new IOException("Unable to close "+namespace+"|"+name+", nothing to close");
558                if (levels == null)
559                        throw new Error("levels = null");
560                if (levels.current() == null)
561                        throw new Error("levels.current() = null");
562                if (levels.current().getName() == null)
563                        throw new Error("levels.current().getName() = null");
564                if (levels.current().getNamespace() == null)
565                        throw new Error("levels.current().getNamespace() = null");
566                if (name == null)
567                        throw new Error("name = null");
568                if (namespace == null)
569                        throw new Error("namespace = null");
570                if (!levels.current().getNamespace().equals(namespace) || !levels.current().getName().equals(name))
571                        throw new IOException("Unable to close "+namespace+"|"+name+", found "+levels.current().getNamespace()+"|"+levels.current().getName());
572                exit();
573        }
574
575        /* (non-Javadoc)
576         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#closeToLevel(int)
577         */
578        @Override
579        public void exitToLevel(int count) throws IOException {
580    checkStarted();
581                while (levels.size() > count)
582                        exit();         
583        }
584
585
586        /* (non-Javadoc)
587         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#close()
588         */
589  @Override
590  public void close() throws IOException {
591    checkStarted();
592    if (!levels.empty()) 
593      throw new IOException("Called close before exiting all opened elements");
594     super.close();
595  }
596    
597  @Override
598  public void end() throws IOException {
599    checkStarted();
600    if (!levels.empty()) 
601      throw new IOException("Called end() before exiting all opened elements");
602     flush();
603  }
604        @Override
605        public void exit() throws IOException {
606                checkStarted();
607                if (levels.empty()) {
608                        throw new IOException("Called exit one too many times");
609                } else {
610                        if (pendingClose) { 
611                                write("/>");
612                                writePendingComment();
613                                pendingClose = false;
614                        } else {
615                                if (levels.current().hasChildren())
616                                        writePretty();
617                                write("</");
618                                if (levels.current().getNamespace() == null)
619                                        write(levels.current().getName());
620                                else
621                                        write(getNSAbbreviation(levels.current().getNamespace())+levels.current().getName());
622                                write('>');
623                        }
624                        levels.pop();
625                }
626        }
627
628        /* (non-Javadoc)
629         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#open(java.lang.String)
630         */
631        @Override
632        public void enter(String name) throws IOException {
633                enter(null, name);
634        }
635
636
637        /* (non-Javadoc)
638         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#element(java.lang.String, java.lang.String, java.lang.String, boolean)
639         */
640        @Override
641        public void element(String namespace, String name, String content, boolean onlyIfNotEmpty) throws IOException {
642                if (!onlyIfNotEmpty || content != null && !content.equals(""))
643                        element(namespace, name, content);
644        }
645
646        /* (non-Javadoc)
647         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#element(java.lang.String, java.lang.String, java.lang.String, java.lang.String)
648         */
649        @Override
650        public void element(String namespace, String name, String content, String comment) throws IOException {
651                if (!XMLUtil.isNMToken(name))
652                        throw new IOException("XML name "+name+" is not valid");
653                enter(namespace, name, comment);
654                text(content);
655                exit();
656        }
657        
658        /* (non-Javadoc)
659         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#element(java.lang.String, java.lang.String, java.lang.String)
660         */
661        @Override
662        public void element(String namespace, String name, String content) throws IOException {
663                if (!XMLUtil.isNMToken(name))
664                        throw new IOException("XML name "+name+" is not valid");
665                enter(namespace, name);
666                text(content);
667                exit();
668        }
669
670        /* (non-Javadoc)
671         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#element(java.lang.String, java.lang.String, boolean)
672         */
673        @Override
674        public void element(String name, String content, boolean onlyIfNotEmpty) throws IOException {
675                if (!onlyIfNotEmpty || content != null && !content.equals(""))
676                        element(null, name, content);
677        }
678
679        /* (non-Javadoc)
680         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#element(java.lang.String, java.lang.String)
681         */
682        @Override
683        public void element(String name, String content) throws IOException {
684                element(null, name, content);
685        }
686
687  @Override
688  public void element(String name) throws IOException {
689    element(null, name, null);
690  }
691
692        /* (non-Javadoc)
693         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#text(java.lang.String)
694         */
695        @Override
696        public void text(String content) throws IOException {
697                text(content, false);
698        }
699
700        /* (non-Javadoc)
701         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#text(java.lang.String, boolean)
702         * 
703         * Replace escapeText()
704         */
705        @Override
706        public void text(String content, boolean dontEscape) throws IOException {
707                checkInElement();
708                if (content != null) {
709                        if (pendingClose) { 
710                                write(">");
711                                writePendingComment();
712                                pendingClose = false;
713                        }
714                        if (dontEscape)
715                                write(content);
716                        else
717                                write(XMLUtil.escapeXML(content, "US-ASCII", false));
718                }
719        }
720
721        /* (non-Javadoc)
722         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#cData(java.lang.String)
723         */
724        @Override
725        public void cData(String text) throws IOException {
726                text("<![CDATA["+text+"]]>");           
727        }
728        
729        /* (non-Javadoc)
730         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#writeBytes(byte[])
731         */
732        @Override
733        public void writeBytes(byte[] bytes) throws IOException {
734                checkInElement();
735                if (pendingClose) { 
736                        write(">");
737                        writePendingComment();
738                        pendingClose = false;
739                }
740                flush();
741                stream.write(bytes);
742        }
743
744
745        /* (non-Javadoc)
746         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#isPretty()
747         */
748        @Override
749        public boolean isPretty() throws IOException {
750                return (levels == null || levels.empty()) ? prettyBase : levels.current().isPretty();
751        }
752
753        /* (non-Javadoc)
754         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#setPretty(boolean)
755         */
756        @Override
757        public void setPretty(boolean pretty) throws IOException {
758                if (levels == null || levels.empty())
759                        this.prettyBase = pretty;
760                else 
761                        levels.current().setPretty(pretty);
762        }
763
764        /* (non-Javadoc)
765         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#startCommentBlock()
766         */
767        @Override
768        public void startCommentBlock() throws IOException {
769                if (levels.inComment())
770                        throw new IOException("cannot nest comments");
771                levels.current().setInComment(true);
772                if (isPretty())
773                        writePretty();
774                write("<!--");
775                if (isPretty())
776                        writePretty();          
777        }
778
779        /* (non-Javadoc)
780         * @see org.eclipse.ohf.utilities.xml.IXMLWriter#endCommentBlock()
781         */
782        @Override
783        public void endCommentBlock() throws IOException {
784                if (!levels.inComment())
785                        throw new IOException("cannot close a comment block when it is open");
786                if (!levels.current().isInComment())
787                        throw new IOException("cannot close a comment block when it is open");
788                if (isPretty())
789                        writePretty();
790                write("-->");
791                if (isPretty())
792                        writePretty();          
793                levels.current().setInComment(false);
794        }
795
796        public boolean isSortAttributes() {
797                return sortAttributes;
798        }
799
800        public void setSortAttributes(boolean sortAttributes) {
801                this.sortAttributes = sortAttributes;
802        }
803
804
805        public boolean isPrettyHeader() {
806                return prettyHeader;
807        }
808
809        public void setPrettyHeader(boolean pretty) {
810                this.prettyHeader = pretty;
811        }
812
813        public int writePretty() throws IOException {
814                return writePretty(true);
815        }
816        
817        public int writePretty(boolean eoln) throws IOException {
818                if (isPretty()) {
819                        if (eoln)
820                                write(lineType == LINE_UNIX ? "\n" : "\r\n");
821                        for (int i = 0; i < levels.size() - 1; i++)
822                                write("  ");
823                        return (levels.size() - 1) * 2;
824                } else
825                        return 0;
826        }
827
828        public int getLineType() {
829                return lineType;
830        }
831
832        public void setLineType(int lineType) {
833                this.lineType = lineType;
834        }
835
836        public boolean isXmlHeader() {
837                return xmlHeader;
838        }
839
840        public void setXmlHeader(boolean xmlHeader) {
841                this.xmlHeader = xmlHeader;
842        }
843
844        public String[] getSpecialAttributeNames() {
845                return specialAttributeNames;
846        }
847
848        public void setSpecialAttributeNames(String[] specialAttributeNames) {
849                this.specialAttributeNames = specialAttributeNames;
850        }
851
852        public int getAttributeLineWrap() {
853                return attributeLineWrap;
854        }
855
856        public void setAttributeLineWrap(int attributeLineWrap) {
857                this.attributeLineWrap = attributeLineWrap;
858        }
859
860        @Override
861        public void escapedText(String content) throws IOException {
862                text("");
863                int i = content.length();
864                if (isPretty())
865                  while (i > 0 && (content.charAt(i-1) == '\r' || content.charAt(i-1) == '\n'))
866                         i--;
867                write(content.substring(0, i));
868        }
869
870  public void processingInstruction(String value) throws IOException {
871    write("<?"+value+"?>");
872    if (isPrettyHeader())
873      write("\r\n");
874  }
875
876  @Override
877  public void link(String href) {
878    // ignore this
879    
880  }
881        
882        
883}
884