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