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