2003-03-15 Atsushi Enomoto <ginga@kit.hi-ho.ne.jp>
[mono.git] / mcs / class / System.XML / System.Xml / XmlTextWriter.cs
index 13bec99bdaa7489f8425915077b15b79dd9a89ba..eb0cf3b1ca4e44293e6f1275616d7d7580f582b9 100644 (file)
@@ -6,6 +6,9 @@
 //
 // (C) 2002 Kral Ferch
 //
+// [FIXME]
+// Document state should be considered.
+//
 
 using System;
 using System.Collections;
@@ -18,10 +21,33 @@ namespace System.Xml
        {
                #region Fields
 
-               protected TextWriter w;
-               protected bool openWriter = true;
-               protected bool openStartElement;
-               protected Stack openElements = new Stack();
+               TextWriter w;
+               bool nullEncoding = false;
+               bool openWriter = true;
+               bool openStartElement = false;
+               bool openStartAttribute = false;
+               bool documentStarted = false;
+               bool namespaces = true;
+               bool openAttribute = false;
+               bool attributeWrittenForElement = false;
+               Stack openElements = new Stack ();
+               Formatting formatting = Formatting.None;
+               int indentation = 2;
+               char indentChar = ' ';
+               string indentChars = "  ";
+               char quoteChar = '\"';
+               int indentLevel = 0;
+               string indentFormatting;
+               Stream baseStream = null;
+               string xmlLang = null;
+               XmlSpace xmlSpace = XmlSpace.None;
+               bool openXmlLang = false;
+               bool openXmlSpace = false;
+               string openElementPrefix;
+               string openElementNS;
+               bool hasRoot = false;
+               Hashtable writtenAttributes = new Hashtable ();
+               bool checkMultipleAttributes = false;
 
                #endregion
 
@@ -30,115 +56,256 @@ namespace System.Xml
                public XmlTextWriter (TextWriter w) : base ()
                {
                        this.w = w;
+                       nullEncoding = (w.Encoding == null);
+                       
+                       try {
+                               baseStream = ((StreamWriter)w).BaseStream;
+                       }
+                       catch (Exception) { }
                }
 
                public XmlTextWriter (Stream w, Encoding encoding) : base ()
                {
+                       if (encoding == null) {
+                               nullEncoding = true;
+                               encoding = new UTF8Encoding ();
+                       }
+
                        this.w = new StreamWriter(w, encoding);
+                       baseStream = w;
                }
 
-               public XmlTextWriter (string filename, Encoding encoding) : base ()
+               public XmlTextWriter (string filename, Encoding encoding) :
+                       this (new FileStream (filename, FileMode.Create, FileAccess.Write, FileShare.None), encoding)
                {
-                       this.w = new StreamWriter(filename, false, encoding);
                }
 
                #endregion
 
                #region Properties
 
-               [MonoTODO]
                public Stream BaseStream {
-                       get { throw new NotImplementedException(); }
+                       get { return baseStream; }
                }
 
 
-               [MonoTODO]
                public Formatting Formatting {
-                       get { throw new NotImplementedException(); }
-                       set { throw new NotImplementedException(); }
+                       get { return formatting; }
+                       set { formatting = value; }
+               }
+
+               private bool IndentingOverriden 
+               {
+                       get {
+                               if (openElements.Count == 0)
+                                       return false;
+                               else
+                                       return (((XmlTextWriterOpenElement)openElements.Peek()).IndentingOverriden);
+                       }
+                       set {
+                               if (openElements.Count > 0)
+                                       ((XmlTextWriterOpenElement)openElements.Peek()).IndentingOverriden = value;
+                       }
                }
 
-               [MonoTODO]
                public int Indentation {
-                       get { throw new NotImplementedException(); }
-                       set { throw new NotImplementedException(); }
+                       get { return indentation; }
+                       set {
+                               indentation = value;
+                               UpdateIndentChars ();
+                       }
                }
 
-               [MonoTODO]
                public char IndentChar {
-                       get { throw new NotImplementedException(); }
-                       set { throw new NotImplementedException(); }
+                       get { return indentChar; }
+                       set {
+                               indentChar = value;
+                               UpdateIndentChars ();
+                       }
                }
 
-               [MonoTODO]
                public bool Namespaces {
-                       get { throw new NotImplementedException(); }
-                       set { throw new NotImplementedException(); }
+                       get { return namespaces; }
+                       set {
+                               if (ws != WriteState.Start)
+                                       throw new InvalidOperationException ("NotInWriteState.");
+                               
+                               namespaces = value;
+                       }
                }
 
-               [MonoTODO]
                public char QuoteChar {
-                       get { throw new NotImplementedException(); }
-                       set { throw new NotImplementedException(); }
+                       get { return quoteChar; }
+                       set {
+                               if ((value != '\'') && (value != '\"'))
+                                       throw new ArgumentException ("This is an invalid XML attribute quote character. Valid attribute quote characters are ' and \".");
+                               
+                               quoteChar = value;
+                       }
                }
 
-               [MonoTODO]
                public override WriteState WriteState {
-                       get { throw new NotImplementedException(); }
+                       get { return ws; }
                }
                
-               [MonoTODO]
                public override string XmlLang {
-                       get { throw new NotImplementedException(); }
+                       get {
+                               string xmlLang = null;
+                               int i;
+
+                               for (i = 0; i < openElements.Count; i++) 
+                               {
+                                       xmlLang = ((XmlTextWriterOpenElement)openElements.ToArray().GetValue(i)).XmlLang;
+                                       if (xmlLang != null)
+                                               break;
+                               }
+
+                               return xmlLang;
+                       }
                }
 
-               [MonoTODO]
                public override XmlSpace XmlSpace {
-                       get { throw new NotImplementedException(); }
+                       get {
+                               XmlSpace xmlSpace = XmlSpace.None;
+                               int i;
+
+                               for (i = 0; i < openElements.Count; i++) 
+                               {
+                                       xmlSpace = ((XmlTextWriterOpenElement)openElements.ToArray().GetValue(i)).XmlSpace;
+                                       if (xmlSpace != XmlSpace.None)
+                                               break;
+                               }
+
+                               return xmlSpace;
+                       }
                }
 
                #endregion
 
                #region Methods
+               private void AddMissingElementXmlns ()
+               {
+                       // output namespace declaration if not exist.
+                       string prefix = openElementPrefix;
+                       string ns = openElementNS;
+                       openElementPrefix = null;
+                       openElementNS = null;
+
+                       // LAMESPEC: If prefix was already assigned another nsuri, then this element's nsuri goes away!
 
-               private void CheckOpenWriter ()
+                       if (ns != null) 
+                       {
+                               string formatXmlns = String.Empty;
+                               if (ns != String.Empty)
+                               {
+                                       string existingPrefix = namespaceManager.LookupPrefix (ns);
+                                       bool addDefaultNamespace = false;
+
+                                       if (existingPrefix == null) 
+                                       {
+                                               namespaceManager.AddNamespace (prefix, ns);
+                                               addDefaultNamespace = true;
+                                       }
+
+                                       if (prefix == String.Empty)
+                                               prefix = existingPrefix;
+
+                                       if (prefix != existingPrefix)
+                                               formatXmlns = String.Format (" xmlns:{0}={1}{2}{1}", prefix, quoteChar, ns);
+                                       else if (addDefaultNamespace)
+                                               formatXmlns = String.Format (" xmlns={0}{1}{0}", quoteChar, ns);
+                               } 
+                               else if ((prefix == String.Empty) && (namespaceManager.LookupNamespace (prefix) != ns)) 
+                               {
+                                       namespaceManager.AddNamespace (prefix, ns);
+                                       formatXmlns = String.Format (" xmlns={0}{0}", quoteChar);
+                               }
+                               if(formatXmlns != String.Empty) {
+                                       string xmlns = formatXmlns.Trim ();
+                                       if (checkMultipleAttributes && !writtenAttributes.Contains (xmlns.Substring (0, xmlns.IndexOf ('='))))
+                                               w.Write(formatXmlns);
+                               }
+                       }
+               }
+
+               private void CheckState ()
                {
                        if (!openWriter) {
-                               throw new InvalidOperationException ();
+                               throw new InvalidOperationException ("The Writer is closed.");
+                       }
+                       if ((documentStarted == true) && (formatting == Formatting.Indented) && (!IndentingOverriden)) {
+                               indentFormatting = w.NewLine;
+                               if (indentLevel > 0) {
+                                       for (int i = 0; i < indentLevel; i++)
+                                               indentFormatting += indentChars;
+                               }
                        }
+                       else
+                               indentFormatting = "";
+
+                       documentStarted = true;
                }
 
-               [MonoTODO("Need to close all open elements and close the underlying streams.")]
                public override void Close ()
                {
+                       CloseOpenAttributeAndElements ();
+
+                       w.Close();
+                       ws = WriteState.Closed;
                        openWriter = false;
                }
 
-               private void CloseStartElement ()
+               private void CloseOpenAttributeAndElements ()
                {
-                       if (openStartElement) 
-                       {
-                               w.Write(">");
-                               openStartElement = false;
+                       if (openAttribute)
+                               WriteEndAttribute ();
+
+                       while (openElements.Count > 0) {
+                               WriteEndElement();
                        }
                }
 
-               [MonoTODO]
+               private void CloseStartElement ()
+               {
+                       if (!openStartElement)
+                               return;
+
+                       AddMissingElementXmlns ();
+
+                       w.Write (">");
+                       ws = WriteState.Content;
+                       openStartElement = false;
+                       attributeWrittenForElement = false;
+                       checkMultipleAttributes = false;
+                       writtenAttributes.Clear ();
+               }
+
                public override void Flush ()
                {
-                       throw new NotImplementedException ();
+                       w.Flush ();
                }
 
-               [MonoTODO]
                public override string LookupPrefix (string ns)
                {
-                       throw new NotImplementedException ();
+                       string prefix = namespaceManager.LookupPrefix (ns);
+
+                       // XmlNamespaceManager has changed to return null when NSURI not found.
+                       // (Contradiction to the documentation.)
+                       //if (prefix == String.Empty)
+                       //      prefix = null;
+                       return prefix;
+               }
+
+               private void UpdateIndentChars ()
+               {
+                       indentChars = "";
+                       for (int i = 0; i < indentation; i++)
+                               indentChars += indentChar;
                }
 
-               [MonoTODO]
                public override void WriteBase64 (byte[] buffer, int index, int count)
                {
-                       throw new NotImplementedException ();
+                       w.Write (Convert.ToBase64String (buffer, index, count));
                }
 
                [MonoTODO]
@@ -149,13 +316,25 @@ namespace System.Xml
 
                public override void WriteCData (string text)
                {
+                       if (text.IndexOf("]]>") > 0)
+                               throw new ArgumentException ();
+
+                       CheckState ();
+                       CloseStartElement ();
+
                        w.Write("<![CDATA[{0}]]>", text);
                }
 
-               [MonoTODO]
                public override void WriteCharEntity (char ch)
                {
-                       throw new NotImplementedException ();
+                       Int16   intCh = (Int16)ch;
+
+                       // Make sure the character is not in the surrogate pair
+                       // character range, 0xd800- 0xdfff
+                       if ((intCh >= -10240) && (intCh <= -8193))
+                               throw new ArgumentException ("Surrogate Pair is invalid.");
+
+                       w.Write("&#x{0:X};", intCh);
                }
 
                [MonoTODO]
@@ -166,35 +345,100 @@ namespace System.Xml
 
                public override void WriteComment (string text)
                {
-                       w.Write("<!--{0}-->", text);
+                       if ((text.EndsWith("-")) || (text.IndexOf("-->") > 0)) {
+                               throw new ArgumentException ();
+                       }
+
+                       CheckState ();
+                       CloseStartElement ();
+
+                       w.Write ("<!--{0}-->", text);
                }
 
-               [MonoTODO]
                public override void WriteDocType (string name, string pubid, string sysid, string subset)
                {
-                       throw new NotImplementedException ();
+                       if (name == null || name.Trim ().Length == 0)
+                               throw new ArgumentException ("Invalid DOCTYPE name", "name");
+
+                       w.Write ("<!DOCTYPE ");
+                       w.Write (name);
+                       if (pubid != null) {
+                               w.Write (String.Format (" PUBLIC {0}{1}{0} {0}{2}{0}", quoteChar, pubid, sysid));
+                       } else if (sysid != null) {
+                               w.Write (String.Format (" SYSTEM {0}{1}{0}", quoteChar, sysid));
+                       }
+
+                       if (subset != null)
+                               w.Write ("[" + subset + "]");
+
+                       w.Write('>');
                }
 
-               [MonoTODO]
                public override void WriteEndAttribute ()
                {
-                       throw new NotImplementedException ();
+                       if (!openAttribute)
+                               throw new InvalidOperationException("Token EndAttribute in state Start would result in an invalid XML document.");
+
+                       CheckState ();
+
+                       if (openXmlLang) {
+                               w.Write (xmlLang);
+                               openXmlLang = false;
+                               ((XmlTextWriterOpenElement)openElements.Peek()).XmlLang = xmlLang;
+                       }
+
+                       if (openXmlSpace) 
+                       {
+                               w.Write (xmlSpace.ToString ().ToLower ());
+                               openXmlSpace = false;
+                               ((XmlTextWriterOpenElement)openElements.Peek()).XmlSpace = xmlSpace;
+                       }
+
+                       w.Write ("{0}", quoteChar);
+
+                       openAttribute = false;
                }
 
-               [MonoTODO]
                public override void WriteEndDocument ()
                {
-                       throw new NotImplementedException ();
+                       CloseOpenAttributeAndElements ();
+
+                       if (!hasRoot)
+                               throw new ArgumentException ("This document does not have a root element.");
+
+                       ws = WriteState.Start;
+                       hasRoot = false;
                }
 
                public override void WriteEndElement ()
                {
+                       WriteEndElementInternal (false);
+               }
+
+               private void WriteEndElementInternal (bool fullEndElement)
+               {
+                       if (openElements.Count == 0)
+                               throw new InvalidOperationException("There was no XML start tag open.");
+
+                       indentLevel--;
+                       CheckState ();
+                       AddMissingElementXmlns ();
+
                        if (openStartElement) {
-                               w.Write(" />");
-                       }
-                       else {
-                               w.Write("</{0}>", openElements.Pop());
+                               if (openAttribute)
+                                       WriteEndAttribute ();
+                               if (fullEndElement)
+                                       w.Write ("></{0}>", ((XmlTextWriterOpenElement)openElements.Peek ()).Name);
+                               else
+                                       w.Write (" />");
+
+                               openElements.Pop ();
+                               openStartElement = false;
+                       } else {
+                               w.Write ("{0}</{1}>", indentFormatting, openElements.Pop ());
                        }
+
+                       namespaceManager.PopScope();
                }
 
                [MonoTODO]
@@ -203,85 +447,267 @@ namespace System.Xml
                        throw new NotImplementedException ();
                }
 
-               [MonoTODO]
                public override void WriteFullEndElement ()
                {
-                       throw new NotImplementedException ();
+                       WriteEndElementInternal (true);
+               }
+
+               private void CheckValidChars (string name, bool firstOnlyLetter)
+               {
+                       foreach (char c in name) {
+                               if (XmlConvert.IsInvalid (c, firstOnlyLetter))
+                                       throw new ArgumentException ("There is an invalid character: '" + c +
+                                                                    "'", "name");
+                       }
                }
 
-               [MonoTODO]
                public override void WriteName (string name)
                {
-                       throw new NotImplementedException ();
+                       CheckValidChars (name, true);
+                       w.Write (name);
                }
 
-               [MonoTODO]
                public override void WriteNmToken (string name)
                {
-                       throw new NotImplementedException ();
+                       CheckValidChars (name, false);
+                       w.Write (name);
                }
 
                public override void WriteProcessingInstruction (string name, string text)
                {
                        if ((name == null) || (name == string.Empty) || (name.IndexOf("?>") > 0) || (text.IndexOf("?>") > 0)) {
-                               throw new ArgumentException();
+                               throw new ArgumentException ();
                        }
 
-                       w.Write("<?{0} {1}?>", name, text);
+                       CheckState ();
+                       CloseStartElement ();
+
+                       w.Write ("{0}<?{1} {2}?>", indentFormatting, name, text);
                }
 
                [MonoTODO]
                public override void WriteQualifiedName (string localName, string ns)
                {
-                       throw new NotImplementedException ();
+                       if (localName == null || localName == String.Empty)
+                               throw new ArgumentException ();
+
+                       CheckState ();
+                       w.Write ("{0}:{1}", ns, localName);
                }
 
-               [MonoTODO]
                public override void WriteRaw (string data)
                {
-                       throw new NotImplementedException ();
+                       WriteStringInternal (data, false);
                }
 
-               [MonoTODO]
                public override void WriteRaw (char[] buffer, int index, int count)
                {
-                       throw new NotImplementedException ();
+                       WriteStringInternal (new string (buffer, index, count), false);
                }
 
-               [MonoTODO]
                public override void WriteStartAttribute (string prefix, string localName, string ns)
                {
-                       throw new NotImplementedException ();
+                       if ((prefix == "xml") && (localName == "lang"))
+                               openXmlLang = true;
+
+                       if ((prefix == "xml") && (localName == "space"))
+                               openXmlSpace = true;
+
+                       if ((prefix == "xmlns") && (localName.ToLower ().StartsWith ("xml")))
+                               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);
+
+                       CheckState ();
+
+                       if (ws == WriteState.Content)
+                               throw new InvalidOperationException ("Token StartAttribute in state " + WriteState + " would result in an invalid XML document.");
+
+                       if (prefix == null)
+                               prefix = String.Empty;
+
+                       if (ns == null)
+                               ns = String.Empty;
+
+                       string formatPrefix = "";
+                       string formatSpace = "";
+
+                       if (ns != String.Empty) 
+                       {
+                               string existingPrefix = namespaceManager.LookupPrefix (ns);
+
+                               if (prefix == String.Empty)
+                                       prefix = (existingPrefix == null) ?
+                                               String.Empty : existingPrefix;
+                       }
+
+                       if (prefix != String.Empty) 
+                       {
+                               formatPrefix = prefix + ":";
+                       }
+
+                       if (openStartElement || attributeWrittenForElement)
+                               formatSpace = " ";
+
+                       // If already written, then break up.
+                       if (checkMultipleAttributes &&
+                               writtenAttributes.Contains (formatPrefix + localName))
+                               return;
+
+                       w.Write ("{0}{1}{2}={3}", formatSpace, formatPrefix, localName, quoteChar);
+                       if (checkMultipleAttributes)
+                               writtenAttributes.Add (formatPrefix + localName, formatPrefix + localName);
+
+                       openAttribute = true;
+                       attributeWrittenForElement = true;
+                       ws = WriteState.Attribute;
+                       if (prefix == String.Empty && localName == "xmlns") {
+                               if (namespaceManager.LookupNamespace (prefix) == null)
+                                       namespaceManager.AddNamespace (prefix, ns);
+                       } else if (prefix == "xmlns") {
+                               if (namespaceManager.LookupNamespace (localName) == null)
+                                       namespaceManager.AddNamespace (localName, ns);
+                       }
                }
 
-               [MonoTODO]
                public override void WriteStartDocument ()
                {
-                       throw new NotImplementedException ();
+                       WriteStartDocument ("");
                }
 
-               [MonoTODO]
                public override void WriteStartDocument (bool standalone)
                {
-                       throw new NotImplementedException ();
+                       string standaloneFormatting;
+
+                       if (standalone == true)
+                               standaloneFormatting = String.Format (" standalone={0}yes{0}", quoteChar);
+                       else
+                               standaloneFormatting = String.Format (" standalone={0}no{0}", quoteChar);
+
+                       WriteStartDocument (standaloneFormatting);
+               }
+
+               private void WriteStartDocument (string standaloneFormatting)
+               {
+                       if (documentStarted == true)
+                               throw new InvalidOperationException("WriteStartDocument should be the first call.");
+
+                       if (hasRoot)
+                               throw new XmlException ("WriteStartDocument called twice.");
+
+                       hasRoot = true;
+
+                       CheckState ();
+
+                       string encodingFormatting = "";
+
+                       if (!nullEncoding) 
+                               encodingFormatting = String.Format (" encoding={0}{1}{0}", quoteChar, w.Encoding.WebName);
+
+                       w.Write("<?xml version={0}1.0{0}{1}{2}?>", quoteChar, encodingFormatting, standaloneFormatting);
+                       ws = WriteState.Prolog;
                }
 
-               [MonoTODO("Not dealing with prefix and ns yet.")]
                public override void WriteStartElement (string prefix, string localName, string ns)
                {
-                       CheckOpenWriter();
-                       CloseStartElement();
-                       w.Write("<{0}", localName);
-                       openElements.Push(localName);
+                       if (!Namespaces && (((prefix != null) && (prefix != String.Empty))
+                               || ((ns != null) && (ns != String.Empty))))
+                               throw new ArgumentException ("Cannot set the namespace if Namespaces is 'false'.");
+
+                       WriteStartElementInternal (prefix, localName, ns);
+               }
+
+               private void WriteStartElementInternal (string prefix, string localName, string ns)
+               {
+                       if ((prefix != null && prefix != String.Empty) && ((ns == null) || (ns == String.Empty)))
+                               throw new ArgumentException ("Cannot use a prefix with an empty namespace.");
+
+                       CheckState ();
+                       CloseStartElement ();
+                       writtenAttributes.Clear ();
+                       checkMultipleAttributes = true;
+                       
+                       if (prefix == null)
+                               prefix = namespaceManager.LookupPrefix (ns);
+                       if (prefix == null)
+                               prefix = String.Empty;
+
+                       string formatPrefix = "";
+
+                       if(ns != null) {
+                               if (prefix != String.Empty)
+                                       formatPrefix = prefix + ":";
+                       }
+
+                       w.Write ("{0}<{1}{2}", indentFormatting, formatPrefix, localName);
+
+                       openElements.Push (new XmlTextWriterOpenElement (formatPrefix + localName));
+                       ws = WriteState.Element;
                        openStartElement = true;
+                       openElementNS = ns;
+                       openElementPrefix = prefix;
+
+                       namespaceManager.PushScope ();
+                       indentLevel++;
                }
 
-               [MonoTODO("Haven't done any entity replacements yet.")]
                public override void WriteString (string text)
                {
-                       CheckOpenWriter();
-                       CloseStartElement();
-                       w.Write(text);
+                       if (ws == WriteState.Prolog)
+                               throw new InvalidOperationException ("Token content in state Prolog would result in an invalid XML document.");
+
+                       WriteStringInternal (text, true);
+               }
+
+               private void WriteStringInternal (string text, bool entitize)
+               {
+                       if (text == null)
+                               text = String.Empty;
+
+                       if (text != String.Empty) 
+                       {
+                               CheckState ();
+
+                               if (entitize)
+                               {
+                                       text = text.Replace ("&", "&amp;");
+                                       text = text.Replace ("<", "&lt;");
+                                       text = text.Replace (">", "&gt;");
+                                       
+                                       if (openAttribute) 
+                                       {
+                                               if (quoteChar == '"')
+                                                       text = text.Replace ("\"", "&quot;");
+                                               else
+                                                       text = text.Replace ("'", "&apos;");
+                                       }
+                               }
+
+                               if (!openAttribute)
+                               {
+                                       IndentingOverriden = true;
+                                       CloseStartElement ();
+                               }
+                               if (!openXmlLang && !openXmlSpace)
+                                       w.Write (text);
+                               else 
+                               {
+                                       if (openXmlLang)
+                                               xmlLang = text;
+                                       else 
+                                       {
+                                               switch (text) 
+                                               {
+                                                       case "default":
+                                                               xmlSpace = XmlSpace.Default;
+                                                               break;
+                                                       case "preserve":
+                                                               xmlSpace = XmlSpace.Preserve;
+                                                               break;
+                                                       default:
+                                                               throw new ArgumentException ("'{0}' is an invalid xml:space value.");
+                                               }
+                                       }
+                               }
+                       }
                }
 
                [MonoTODO]
@@ -290,10 +716,14 @@ namespace System.Xml
                        throw new NotImplementedException ();
                }
 
-               [MonoTODO]
                public override void WriteWhitespace (string ws)
                {
-                       throw new NotImplementedException ();
+                       foreach (char c in ws) {
+                               if ((c != ' ') && (c != '\t') && (c != '\r') && (c != '\n'))
+                                       throw new ArgumentException ();
+                       }
+
+                       w.Write (ws);
                }
 
                #endregion