2003-03-05 Atsushi Enomoto <ginga@kit.hi-ho.ne.jp>
[mono.git] / mcs / class / System.XML / System.Xml / XmlTextWriter.cs
1 //
2 // System.Xml.XmlTextWriter
3 //
4 // Author:
5 //   Kral Ferch <kral_ferch@hotmail.com>
6 //
7 // (C) 2002 Kral Ferch
8 //
9
10 using System;
11 using System.Collections;
12 using System.IO;
13 using System.Text;
14
15 namespace System.Xml
16 {
17         public class XmlTextWriter : XmlWriter
18         {
19                 #region Fields
20
21                 TextWriter w;
22                 bool nullEncoding = false;
23                 bool openWriter = true;
24                 bool openStartElement = false;
25                 bool openStartAttribute = false;
26                 bool documentStarted = false;
27                 bool namespaces = true;
28                 bool openAttribute = false;
29                 bool attributeWrittenForElement = false;
30                 Stack openElements = new Stack ();
31                 Formatting formatting = Formatting.None;
32                 int indentation = 2;
33                 char indentChar = ' ';
34                 string indentChars = "  ";
35                 char quoteChar = '\"';
36                 int indentLevel = 0;
37                 string indentFormatting;
38                 Stream baseStream = null;
39                 string xmlLang = null;
40                 XmlSpace xmlSpace = XmlSpace.None;
41                 bool openXmlLang = false;
42                 bool openXmlSpace = false;
43                 string openElementPrefix;
44                 string openElementNS;
45                 bool hasRoot = false;
46                 Hashtable writtenAttributes = new Hashtable ();
47                 bool checkMultipleAttributes = false;
48
49                 #endregion
50
51                 #region Constructors
52
53                 public XmlTextWriter (TextWriter w) : base ()
54                 {
55                         this.w = w;
56                         nullEncoding = (w.Encoding == null);
57                         
58                         try {
59                                 baseStream = ((StreamWriter)w).BaseStream;
60                         }
61                         catch (Exception) { }
62                 }
63
64                 public XmlTextWriter (Stream w, Encoding encoding) : base ()
65                 {
66                         if (encoding == null) {
67                                 nullEncoding = true;
68                                 encoding = new UTF8Encoding ();
69                         }
70
71                         this.w = new StreamWriter(w, encoding);
72                         baseStream = w;
73                 }
74
75                 public XmlTextWriter (string filename, Encoding encoding) :
76                         this (new FileStream (filename, FileMode.Create, FileAccess.Write, FileShare.None), encoding)
77                 {
78                 }
79
80                 #endregion
81
82                 #region Properties
83
84                 public Stream BaseStream {
85                         get { return baseStream; }
86                 }
87
88
89                 public Formatting Formatting {
90                         get { return formatting; }
91                         set { formatting = value; }
92                 }
93
94                 private bool IndentingOverriden 
95                 {
96                         get {
97                                 if (openElements.Count == 0)
98                                         return false;
99                                 else
100                                         return (((XmlTextWriterOpenElement)openElements.Peek()).IndentingOverriden);
101                         }
102                         set {
103                                 if (openElements.Count > 0)
104                                         ((XmlTextWriterOpenElement)openElements.Peek()).IndentingOverriden = value;
105                         }
106                 }
107
108                 public int Indentation {
109                         get { return indentation; }
110                         set {
111                                 indentation = value;
112                                 UpdateIndentChars ();
113                         }
114                 }
115
116                 public char IndentChar {
117                         get { return indentChar; }
118                         set {
119                                 indentChar = value;
120                                 UpdateIndentChars ();
121                         }
122                 }
123
124                 public bool Namespaces {
125                         get { return namespaces; }
126                         set {
127                                 if (ws != WriteState.Start)
128                                         throw new InvalidOperationException ("NotInWriteState.");
129                                 
130                                 namespaces = value;
131                         }
132                 }
133
134                 public char QuoteChar {
135                         get { return quoteChar; }
136                         set {
137                                 if ((value != '\'') && (value != '\"'))
138                                         throw new ArgumentException ("This is an invalid XML attribute quote character. Valid attribute quote characters are ' and \".");
139                                 
140                                 quoteChar = value;
141                         }
142                 }
143
144                 public override WriteState WriteState {
145                         get { return ws; }
146                 }
147                 
148                 public override string XmlLang {
149                         get {
150                                 string xmlLang = null;
151                                 int i;
152
153                                 for (i = 0; i < openElements.Count; i++) 
154                                 {
155                                         xmlLang = ((XmlTextWriterOpenElement)openElements.ToArray().GetValue(i)).XmlLang;
156                                         if (xmlLang != null)
157                                                 break;
158                                 }
159
160                                 return xmlLang;
161                         }
162                 }
163
164                 public override XmlSpace XmlSpace {
165                         get {
166                                 XmlSpace xmlSpace = XmlSpace.None;
167                                 int i;
168
169                                 for (i = 0; i < openElements.Count; i++) 
170                                 {
171                                         xmlSpace = ((XmlTextWriterOpenElement)openElements.ToArray().GetValue(i)).XmlSpace;
172                                         if (xmlSpace != XmlSpace.None)
173                                                 break;
174                                 }
175
176                                 return xmlSpace;
177                         }
178                 }
179
180                 #endregion
181
182                 #region Methods
183                 private void AddMissingElementXmlns ()
184                 {
185                         // output namespace declaration if not exist.
186                         string prefix = openElementPrefix;
187                         string ns = openElementNS;
188                         openElementPrefix = null;
189                         openElementNS = null;
190
191                         // LAMESPEC: If prefix was already assigned another nsuri, then this element's nsuri goes away!
192
193                         if (ns != null) 
194                         {
195                                 string formatXmlns = String.Empty;
196                                 if (ns != String.Empty)
197                                 {
198                                         string existingPrefix = namespaceManager.LookupPrefix (ns);
199                                         bool addDefaultNamespace = false;
200
201                                         if (existingPrefix == null) 
202                                         {
203                                                 namespaceManager.AddNamespace (prefix, ns);
204                                                 addDefaultNamespace = true;
205                                         }
206
207                                         if (prefix == String.Empty)
208                                                 prefix = existingPrefix;
209
210                                         if (prefix != existingPrefix)
211                                                 formatXmlns = String.Format (" xmlns:{0}={1}{2}{1}", prefix, quoteChar, ns);
212                                         else if (addDefaultNamespace)
213                                                 formatXmlns = String.Format (" xmlns={0}{1}{0}", quoteChar, ns);
214                                 } 
215                                 else if ((prefix == String.Empty) && (namespaceManager.LookupNamespace (prefix) != ns)) 
216                                 {
217                                         namespaceManager.AddNamespace (prefix, ns);
218                                         formatXmlns = String.Format (" xmlns={0}{0}", quoteChar);
219                                 }
220                                 if(formatXmlns != String.Empty) {
221                                         string xmlns = formatXmlns.Trim ();
222                                         if (checkMultipleAttributes && !writtenAttributes.Contains (xmlns.Substring (0, xmlns.IndexOf ('='))))
223                                                 w.Write(formatXmlns);
224                                 }
225                         }
226                 }
227
228                 private void CheckState ()
229                 {
230                         if (!openWriter) {
231                                 throw new InvalidOperationException ("The Writer is closed.");
232                         }
233                         if ((documentStarted == true) && (formatting == Formatting.Indented) && (!IndentingOverriden)) {
234                                 indentFormatting = w.NewLine;
235                                 if (indentLevel > 0) {
236                                         for (int i = 0; i < indentLevel; i++)
237                                                 indentFormatting += indentChars;
238                                 }
239                         }
240                         else
241                                 indentFormatting = "";
242
243                         documentStarted = true;
244                 }
245
246                 public override void Close ()
247                 {
248                         CloseOpenAttributeAndElements ();
249
250                         w.Close();
251                         ws = WriteState.Closed;
252                         openWriter = false;
253                 }
254
255                 private void CloseOpenAttributeAndElements ()
256                 {
257                         if (openAttribute)
258                                 WriteEndAttribute ();
259
260                         while (openElements.Count > 0) {
261                                 WriteEndElement();
262                         }
263                 }
264
265                 private void CloseStartElement ()
266                 {
267                         if (!openStartElement)
268                                 return;
269
270                         AddMissingElementXmlns ();
271
272                         w.Write (">");
273                         ws = WriteState.Content;
274                         openStartElement = false;
275                         attributeWrittenForElement = false;
276                         checkMultipleAttributes = false;
277                         writtenAttributes.Clear ();
278                 }
279
280                 public override void Flush ()
281                 {
282                         w.Flush ();
283                 }
284
285                 public override string LookupPrefix (string ns)
286                 {
287                         string prefix = namespaceManager.LookupPrefix (ns);
288
289                         // XmlNamespaceManager has changed to return null when NSURI not found.
290                         // (Contradiction to the documentation.)
291                         //if (prefix == String.Empty)
292                         //      prefix = null;
293                         return prefix;
294                 }
295
296                 private void UpdateIndentChars ()
297                 {
298                         indentChars = "";
299                         for (int i = 0; i < indentation; i++)
300                                 indentChars += indentChar;
301                 }
302
303                 public override void WriteBase64 (byte[] buffer, int index, int count)
304                 {
305                         w.Write (Convert.ToBase64String (buffer, index, count));
306                 }
307
308                 [MonoTODO]
309                 public override void WriteBinHex (byte[] buffer, int index, int count)
310                 {
311                         throw new NotImplementedException ();
312                 }
313
314                 public override void WriteCData (string text)
315                 {
316                         if (text.IndexOf("]]>") > 0)
317                                 throw new ArgumentException ();
318
319                         CheckState ();
320                         CloseStartElement ();
321
322                         w.Write("<![CDATA[{0}]]>", text);
323                 }
324
325                 public override void WriteCharEntity (char ch)
326                 {
327                         Int16   intCh = (Int16)ch;
328
329                         // Make sure the character is not in the surrogate pair
330                         // character range, 0xd800- 0xdfff
331                         if ((intCh >= -10240) && (intCh <= -8193))
332                                 throw new ArgumentException ("Surrogate Pair is invalid.");
333
334                         w.Write("&#x{0:X};", intCh);
335                 }
336
337                 [MonoTODO]
338                 public override void WriteChars (char[] buffer, int index, int count)
339                 {
340                         throw new NotImplementedException ();
341                 }
342
343                 public override void WriteComment (string text)
344                 {
345                         if ((text.EndsWith("-")) || (text.IndexOf("-->") > 0)) {
346                                 throw new ArgumentException ();
347                         }
348
349                         CheckState ();
350                         CloseStartElement ();
351
352                         w.Write ("<!--{0}-->", text);
353                 }
354
355                 public override void WriteDocType (string name, string pubid, string sysid, string subset)
356                 {
357                         if (name == null || name.Trim ().Length == 0)
358                                 throw new ArgumentException ("Invalid DOCTYPE name", "name");
359
360                         w.Write ("<!DOCTYPE ");
361                         w.Write (name);
362                         if (pubid != null) {
363                                 w.Write (String.Format (" PUBLIC {0}{1}{0} {0}{2}{0}", quoteChar, pubid, sysid));
364                         } else if (sysid != null) {
365                                 w.Write (String.Format (" SYSTEM {0}{1}{0}", quoteChar, sysid));
366                         }
367
368                         if (subset != null)
369                                 w.Write ("[" + subset + "]");
370
371                         w.Write('>');
372                 }
373
374                 public override void WriteEndAttribute ()
375                 {
376                         if (!openAttribute)
377                                 throw new InvalidOperationException("Token EndAttribute in state Start would result in an invalid XML document.");
378
379                         CheckState ();
380
381                         if (openXmlLang) {
382                                 w.Write (xmlLang);
383                                 openXmlLang = false;
384                                 ((XmlTextWriterOpenElement)openElements.Peek()).XmlLang = xmlLang;
385                         }
386
387                         if (openXmlSpace) 
388                         {
389                                 w.Write (xmlSpace.ToString ().ToLower ());
390                                 openXmlSpace = false;
391                                 ((XmlTextWriterOpenElement)openElements.Peek()).XmlSpace = xmlSpace;
392                         }
393
394                         w.Write ("{0}", quoteChar);
395
396                         openAttribute = false;
397                 }
398
399                 public override void WriteEndDocument ()
400                 {
401                         CloseOpenAttributeAndElements ();
402
403                         if (!hasRoot)
404                                 throw new ArgumentException ("This document does not have a root element.");
405
406                         ws = WriteState.Start;
407                         hasRoot = false;
408                 }
409
410                 public override void WriteEndElement ()
411                 {
412                         WriteEndElementInternal (false);
413                 }
414
415                 private void WriteEndElementInternal (bool fullEndElement)
416                 {
417                         if (openElements.Count == 0)
418                                 throw new InvalidOperationException("There was no XML start tag open.");
419
420                         indentLevel--;
421                         CheckState ();
422                         AddMissingElementXmlns ();
423
424                         if (openStartElement) {
425                                 if (openAttribute)
426                                         WriteEndAttribute ();
427                                 if (fullEndElement)
428                                         w.Write ("></{0}>", ((XmlTextWriterOpenElement)openElements.Peek ()).Name);
429                                 else
430                                         w.Write (" />");
431
432                                 openElements.Pop ();
433                                 openStartElement = false;
434                         } else {
435                                 w.Write ("{0}</{1}>", indentFormatting, openElements.Pop ());
436                         }
437
438                         namespaceManager.PopScope();
439                 }
440
441                 [MonoTODO]
442                 public override void WriteEntityRef (string name)
443                 {
444                         throw new NotImplementedException ();
445                 }
446
447                 public override void WriteFullEndElement ()
448                 {
449                         WriteEndElementInternal (true);
450                 }
451
452                 private void CheckValidChars (string name, bool firstOnlyLetter)
453                 {
454                         foreach (char c in name) {
455                                 if (XmlConvert.IsInvalid (c, firstOnlyLetter))
456                                         throw new ArgumentException ("There is an invalid character: '" + c +
457                                                                      "'", "name");
458                         }
459                 }
460
461                 public override void WriteName (string name)
462                 {
463                         CheckValidChars (name, true);
464                         w.Write (name);
465                 }
466
467                 public override void WriteNmToken (string name)
468                 {
469                         CheckValidChars (name, false);
470                         w.Write (name);
471                 }
472
473                 public override void WriteProcessingInstruction (string name, string text)
474                 {
475                         if ((name == null) || (name == string.Empty) || (name.IndexOf("?>") > 0) || (text.IndexOf("?>") > 0)) {
476                                 throw new ArgumentException ();
477                         }
478
479                         CheckState ();
480                         CloseStartElement ();
481
482                         w.Write ("{0}<?{1} {2}?>", indentFormatting, name, text);
483                 }
484
485                 [MonoTODO]
486                 public override void WriteQualifiedName (string localName, string ns)
487                 {
488                         if (localName == null || localName == String.Empty)
489                                 throw new ArgumentException ();
490
491                         CheckState ();
492                         w.Write ("{0}:{1}", ns, localName);
493                 }
494
495                 public override void WriteRaw (string data)
496                 {
497                         WriteStringInternal (data, false);
498                 }
499
500                 public override void WriteRaw (char[] buffer, int index, int count)
501                 {
502                         WriteStringInternal (new string (buffer, index, count), false);
503                 }
504
505                 public override void WriteStartAttribute (string prefix, string localName, string ns)
506                 {
507                         if ((prefix == "xml") && (localName == "lang"))
508                                 openXmlLang = true;
509
510                         if ((prefix == "xml") && (localName == "space"))
511                                 openXmlSpace = true;
512
513                         if ((prefix == "xmlns") && (localName.ToLower ().StartsWith ("xml")))
514                                 throw new ArgumentException ("Prefixes beginning with \"xml\" (regardless of whether the characters are uppercase, lowercase, or some combination thereof) are reserved for use by XML: " + prefix + ":" + localName);
515
516                         CheckState ();
517
518                         if (ws == WriteState.Content)
519                                 throw new InvalidOperationException ("Token StartAttribute in state " + WriteState + " would result in an invalid XML document.");
520
521                         if (prefix == null)
522                                 prefix = String.Empty;
523
524                         if (ns == null)
525                                 ns = String.Empty;
526
527                         string formatPrefix = "";
528                         string formatSpace = "";
529
530                         if (ns != String.Empty) 
531                         {
532                                 string existingPrefix = namespaceManager.LookupPrefix (ns);
533
534                                 if (prefix == String.Empty)
535                                         prefix = (existingPrefix == null) ?
536                                                 String.Empty : existingPrefix;
537                         }
538
539                         if (prefix != String.Empty) 
540                         {
541                                 formatPrefix = prefix + ":";
542                         }
543
544                         if (openStartElement || attributeWrittenForElement)
545                                 formatSpace = " ";
546
547                         // If already written, then break up.
548                         if (checkMultipleAttributes &&
549                                 writtenAttributes.Contains (formatPrefix + localName))
550                                 return;
551
552                         w.Write ("{0}{1}{2}={3}", formatSpace, formatPrefix, localName, quoteChar);
553                         if (checkMultipleAttributes)
554                                 writtenAttributes.Add (formatPrefix + localName, formatPrefix + localName);
555
556                         openAttribute = true;
557                         attributeWrittenForElement = true;
558                         ws = WriteState.Attribute;
559                         if (prefix == String.Empty && localName == "xmlns") {
560                                 if (namespaceManager.LookupNamespace (prefix) == null)
561                                         namespaceManager.AddNamespace (prefix, ns);
562                         } else if (prefix == "xmlns") {
563                                 if (namespaceManager.LookupNamespace (localName) == null)
564                                         namespaceManager.AddNamespace (localName, ns);
565                         }
566                 }
567
568                 public override void WriteStartDocument ()
569                 {
570                         WriteStartDocument ("");
571                 }
572
573                 public override void WriteStartDocument (bool standalone)
574                 {
575                         string standaloneFormatting;
576
577                         if (standalone == true)
578                                 standaloneFormatting = String.Format (" standalone={0}yes{0}", quoteChar);
579                         else
580                                 standaloneFormatting = String.Format (" standalone={0}no{0}", quoteChar);
581
582                         WriteStartDocument (standaloneFormatting);
583                 }
584
585                 private void WriteStartDocument (string standaloneFormatting)
586                 {
587                         if (documentStarted == true)
588                                 throw new InvalidOperationException("WriteStartDocument should be the first call.");
589
590                         if (hasRoot)
591                                 throw new XmlException ("WriteStartDocument called twice.");
592
593                         hasRoot = true;
594
595                         CheckState ();
596
597                         string encodingFormatting = "";
598
599                         if (!nullEncoding) 
600                                 encodingFormatting = String.Format (" encoding={0}{1}{0}", quoteChar, w.Encoding.HeaderName);
601
602                         w.Write("<?xml version={0}1.0{0}{1}{2}?>", quoteChar, encodingFormatting, standaloneFormatting);
603                         ws = WriteState.Prolog;
604                 }
605
606                 public override void WriteStartElement (string prefix, string localName, string ns)
607                 {
608                         if (!Namespaces && (((prefix != null) && (prefix != String.Empty))
609                                 || ((ns != null) && (ns != String.Empty))))
610                                 throw new ArgumentException ("Cannot set the namespace if Namespaces is 'false'.");
611
612                         WriteStartElementInternal (prefix, localName, ns);
613                 }
614
615                 private void WriteStartElementInternal (string prefix, string localName, string ns)
616                 {
617                         if ((prefix != null && prefix != String.Empty) && ((ns == null) || (ns == String.Empty)))
618                                 throw new ArgumentException ("Cannot use a prefix with an empty namespace.");
619
620                         CheckState ();
621                         CloseStartElement ();
622                         writtenAttributes.Clear ();
623                         checkMultipleAttributes = true;
624                         
625                         if (prefix == null)
626                                 prefix = namespaceManager.LookupPrefix (ns);
627                         if (prefix == null)
628                                 prefix = String.Empty;
629
630                         string formatPrefix = "";
631
632                         if(ns != null) {
633                                 if (prefix != String.Empty)
634                                         formatPrefix = prefix + ":";
635                         }
636
637                         w.Write ("{0}<{1}{2}", indentFormatting, formatPrefix, localName);
638
639                         openElements.Push (new XmlTextWriterOpenElement (formatPrefix + localName));
640                         ws = WriteState.Element;
641                         openStartElement = true;
642                         openElementNS = ns;
643                         openElementPrefix = prefix;
644
645                         namespaceManager.PushScope ();
646                         indentLevel++;
647                 }
648
649                 public override void WriteString (string text)
650                 {
651                         if (ws == WriteState.Prolog)
652                                 throw new InvalidOperationException ("Token content in state Prolog would result in an invalid XML document.");
653
654                         WriteStringInternal (text, true);
655                 }
656
657                 private void WriteStringInternal (string text, bool entitize)
658                 {
659                         if (text == null)
660                                 text = String.Empty;
661
662                         if (text != String.Empty) 
663                         {
664                                 CheckState ();
665
666                                 if (entitize)
667                                 {
668                                         text = text.Replace ("&", "&amp;");
669                                         text = text.Replace ("<", "&lt;");
670                                         text = text.Replace (">", "&gt;");
671                                         
672                                         if (openAttribute) 
673                                         {
674                                                 if (quoteChar == '"')
675                                                         text = text.Replace ("\"", "&quot;");
676                                                 else
677                                                         text = text.Replace ("'", "&apos;");
678                                         }
679                                 }
680
681                                 if (!openAttribute)
682                                 {
683                                         IndentingOverriden = true;
684                                         CloseStartElement ();
685                                 }
686                                 if (!openXmlLang && !openXmlSpace)
687                                         w.Write (text);
688                                 else 
689                                 {
690                                         if (openXmlLang)
691                                                 xmlLang = text;
692                                         else 
693                                         {
694                                                 switch (text) 
695                                                 {
696                                                         case "default":
697                                                                 xmlSpace = XmlSpace.Default;
698                                                                 break;
699                                                         case "preserve":
700                                                                 xmlSpace = XmlSpace.Preserve;
701                                                                 break;
702                                                         default:
703                                                                 throw new ArgumentException ("'{0}' is an invalid xml:space value.");
704                                                 }
705                                         }
706                                 }
707                         }
708                 }
709
710                 [MonoTODO]
711                 public override void WriteSurrogateCharEntity (char lowChar, char highChar)
712                 {
713                         throw new NotImplementedException ();
714                 }
715
716                 public override void WriteWhitespace (string ws)
717                 {
718                         foreach (char c in ws) {
719                                 if ((c != ' ') && (c != '\t') && (c != '\r') && (c != '\n'))
720                                         throw new ArgumentException ();
721                         }
722
723                         w.Write (ws);
724                 }
725
726                 #endregion
727         }
728 }