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