// // mcs/class/System.Data/System.Xml/XmlDataDocument.cs // // Purpose: Provides a W3C XML DOM Document to interact with // relational data in a DataSet // // class: XmlDataDocument // assembly: System.Data.dll // namespace: System.Xml // // Author: // Daniel Morgan // Ville Palo // Atsushi Enomoto // // (c)copyright 2002 Daniel Morgan // (c)copyright 2003 Ville Palo // (c)2004 Novell Inc. // // XmlDataDocument is included within the Mono Class Library. // // // Copyright (C) 2004 Novell, Inc (http://www.novell.com) // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // using System; using System.Data; using System.IO; using System.Text; using System.Xml.XPath; using System.Collections; using System.Globalization; using System.ComponentModel; namespace System.Xml { public class XmlDataDocument : XmlDocument { // Should we consider overriding CloneNode() ? By default // base CloneNode() will be invoked and thus no DataRow conflict // would happen, that sounds the best (that means, no mapped // DataRow will be provided). internal class XmlDataElement : XmlElement { DataRow row; internal XmlDataElement (DataRow row, string prefix, string localName, string ns, XmlDataDocument doc) : base (prefix, localName, ns, doc) { this.row = row; // Embed row ID only when the element is mapped to // certain DataRow. if (row != null) { row.DataElement = this; row.XmlRowID = doc.dataRowID; doc.dataRowIDList.Add (row.XmlRowID); // It should not be done here. The node is detached // dt.Rows.Add (tempRow); doc.dataRowID++; } } internal DataRow DataRow { get { return row; } } } #region Fields private DataSet dataSet; private int dataRowID = 1; private ArrayList dataRowIDList = new ArrayList (); // this keeps whether table change events should be handles private bool raiseDataSetEvents = true; private bool raiseDocumentEvents = true; // this is needed for inserting new row to datatable via xml private Hashtable TempTable = new Hashtable (); DataColumnChangeEventHandler columnChanged; DataRowChangeEventHandler rowDeleted; DataRowChangeEventHandler rowChanged; CollectionChangeEventHandler tablesChanged; #endregion // Fields #region Constructors public XmlDataDocument () { InitDelegateFields (); dataSet = new DataSet(); dataSet._xmlDataDocument = this; dataSet.Tables.CollectionChanged += tablesChanged; AddXmlDocumentListeners (); DataSet.EnforceConstraints = false; } public XmlDataDocument (DataSet dataset) { if (dataset == null) throw new ArgumentException ("Parameter dataset cannot be null."); if (dataset._xmlDataDocument != null) throw new ArgumentException ("DataSet cannot be associated with two or more XmlDataDocument."); InitDelegateFields (); this.dataSet = dataset; this.dataSet._xmlDataDocument = this; XmlElement docElem = CreateElement (dataSet.Prefix, dataSet.DataSetName, dataSet.Namespace); foreach (DataTable dt in dataSet.Tables) { if (dt.ParentRelations.Count > 0) continue; // don't add them here FillNodeRows (docElem, dt, dt.Rows); } // This seems required to avoid Load() error when for // example empty DataSet will be filled on Load(). if (docElem.ChildNodes.Count > 0) AppendChild (docElem); foreach (DataTable dt in dataSet.Tables) { dt.ColumnChanged += columnChanged; dt.RowDeleted += rowDeleted; dt.RowChanged += rowChanged; } AddXmlDocumentListeners (); } // bool clone. If we are cloning XmlDataDocument then clone should be true. // FIXME: shouldn't DataSet be mapped to at most one document?? private XmlDataDocument (DataSet dataset, bool clone) { InitDelegateFields (); this.dataSet = dataset; this.dataSet._xmlDataDocument = this; foreach (DataTable Table in DataSet.Tables) { foreach (DataRow Row in Table.Rows) { Row.XmlRowID = dataRowID; dataRowIDList.Add (dataRowID); dataRowID++; } } AddXmlDocumentListeners (); foreach (DataTable Table in dataSet.Tables) { Table.ColumnChanged += columnChanged; Table.RowDeleted += rowDeleted; Table.RowChanged += rowChanged; } } #endregion // Constructors #region Public Properties public DataSet DataSet { get { return dataSet; } } #endregion // Public Properties #region Public Methods private void FillNodeRows (XmlElement parent, DataTable dt, ICollection rows) { foreach (DataRow dr in dt.Rows) { XmlDataElement el = new XmlDataElement (dr, dt.Prefix, dt.TableName, dt.Namespace, this); for (int i = 0; i < dt.Columns.Count; i++) { DataColumn col = dt.Columns [i]; string value = dr.IsNull (col) ? String.Empty : dr [col].ToString (); switch (col.ColumnMapping) { case MappingType.Element: XmlElement cel = CreateElement (col.Prefix, col.ColumnName, col.Namespace); cel.InnerText = value; el.AppendChild (cel); break; case MappingType.Attribute: XmlAttribute a = CreateAttribute (col.Prefix, col.ColumnName, col.Namespace); a.Value = value; el.SetAttributeNode (a); break; case MappingType.SimpleContent: XmlText t = CreateTextNode (value); el.AppendChild (t); break; } } foreach (DataRelation rel in dt.ChildRelations) FillNodeRows (el, rel.ChildTable, dr.GetChildRows (rel)); parent.AppendChild (el); } } public override XmlNode CloneNode (bool deep) { XmlDataDocument Document; if (deep) Document = new XmlDataDocument (DataSet.Copy (), true); else Document = new XmlDataDocument (DataSet.Clone (), true); Document.RemoveXmlDocumentListeners (); Document.PreserveWhitespace = PreserveWhitespace; if (deep) { foreach(XmlNode n in ChildNodes) Document.AppendChild (Document.ImportNode (n, deep)); } Document.AddXmlDocumentListeners (); return Document; } #region overloaded CreateElement methods public override XmlElement CreateElement( string prefix, string localName, string namespaceURI) { DataTable dt = DataSet.Tables [localName]; DataRow row = dt != null ? dt.NewRow () : null; if (row != null) return GetElementFromRow (row); else return base.CreateElement (prefix, localName, namespaceURI); } #endregion // overloaded CreateElement Methods // It is not supported in XmlDataDocument public override XmlEntityReference CreateEntityReference(string name) { throw new NotSupportedException (); } // It is not supported in XmlDataDocument public override XmlElement GetElementById (string elemId) { throw new NotSupportedException (); } // get the XmlElement associated with the DataRow public XmlElement GetElementFromRow (DataRow r) { return r.DataElement; } // get the DataRow associated with the XmlElement public DataRow GetRowFromElement (XmlElement e) { XmlDataElement el = e as XmlDataElement; if (el == null) return null; return el.DataRow; } #region overload Load methods public override void Load(Stream inStream) { Load (new XmlTextReader (inStream)); } public override void Load(string filename) { Load (new XmlTextReader (filename)); } public override void Load(TextReader txtReader) { Load (new XmlTextReader (txtReader)); } public override void Load (XmlReader reader) { if (DocumentElement != null) throw new InvalidOperationException ("XmlDataDocument does not support multi-time loading. New XmlDadaDocument is always required."); bool OldEC = DataSet.EnforceConstraints; DataSet.EnforceConstraints = false; dataSet.Tables.CollectionChanged -= tablesChanged; base.Load (reader); DataSet.EnforceConstraints = OldEC; dataSet.Tables.CollectionChanged += tablesChanged; } #endregion // overloaded Load methods #endregion // Public Methods #region Protected Methods [MonoTODO ("Create optimized XPathNavigator")] protected override XPathNavigator CreateNavigator(XmlNode node) { return base.CreateNavigator (node); } #endregion // Protected Methods #region XmlDocument event handlers private void OnNodeChanging (object sender, XmlNodeChangedEventArgs args) { if (!this.raiseDocumentEvents) return; if (DataSet.EnforceConstraints) throw new InvalidOperationException (Locale.GetText ("Please set DataSet.EnforceConstraints == false before trying to edit XmlDataDocument using XML operations.")); } // Invoked when XmlNode is changed colum is changed private void OnNodeChanged (object sender, XmlNodeChangedEventArgs args) { if (!raiseDocumentEvents) return; bool escapedRaiseDataSetEvents = raiseDataSetEvents; raiseDataSetEvents = false; try { if (args.Node == null) return; DataRow row = GetRowFromElement ((XmlElement)args.Node.ParentNode.ParentNode); if (row == null) return; if (!row.Table.Columns.Contains (args.Node.ParentNode.Name)) return; if (row [args.Node.ParentNode.Name].ToString () != args.Node.InnerText) { DataColumn col = row.Table.Columns [args.Node.ParentNode.Name]; row [col] = StringToObject (col.DataType, args.Node.InnerText); } } finally { raiseDataSetEvents = escapedRaiseDataSetEvents; } } private void OnNodeRemoving (object sender, XmlNodeChangedEventArgs args) { if (!this.raiseDocumentEvents) return; if (DataSet.EnforceConstraints) throw new InvalidOperationException (Locale.GetText ("Please set DataSet.EnforceConstraints == false before trying to edit XmlDataDocument using XML operations.")); } // Invoked when XmlNode is removed private void OnNodeRemoved (object sender, XmlNodeChangedEventArgs args) { if (!raiseDocumentEvents) return; bool escapedRaiseDataSetEvents = raiseDataSetEvents; raiseDataSetEvents = false; try { if (args.OldParent == null) return; XmlElement oldParentElem = args.OldParent as XmlElement; if (oldParentElem == null) return; // detach child row (if exists) XmlElement childElem = args.Node as XmlElement; if (childElem != null) { DataRow childRow = GetRowFromElement (childElem); if (childRow != null) childRow.Table.Rows.Remove (childRow); } DataRow row = GetRowFromElement (oldParentElem); if (row == null) return ; row [args.Node.Name] = null; } finally { raiseDataSetEvents = escapedRaiseDataSetEvents; } } private void OnNodeInserting (object sender, XmlNodeChangedEventArgs args) { if (!this.raiseDocumentEvents) return; if (DataSet.EnforceConstraints) throw new InvalidOperationException (Locale.GetText ("Please set DataSet.EnforceConstraints == false before trying to edit XmlDataDocument using XML operations.")); } private void OnNodeInserted (object sender, XmlNodeChangedEventArgs args) { if (!raiseDocumentEvents) return; bool escapedRaiseDataSetEvents = raiseDataSetEvents; raiseDataSetEvents = false; // If the parent node is mapped to a DataTable, then // add a DataRow and map the parent element to it. // // AND If the child node is mapped to a DataTable, then // 1. if it is mapped to a DataTable and relation, add // a new DataRow and map the child element to it. // 2. if it is mapped to a DataColumn, set the column // value of the parent DataRow as the child try { if (! (args.NewParent is XmlElement)) { // i.e. adding document element foreach (XmlNode table in args.Node.ChildNodes) CheckDescendantRelationship (table); return; } DataRow row = GetRowFromElement (args.NewParent as XmlElement); if (row == null) { // That happens only when adding table to existing DocumentElement (aka DataSet element) if (args.NewParent == DocumentElement) CheckDescendantRelationship (args.Node); return; } XmlAttribute attr = args.Node as XmlAttribute; if (attr != null) { // fill attribute value DataColumn col = row.Table.Columns [attr.LocalName]; if (col != null) row [col] = StringToObject (col.DataType, args.Node.Value); } else { DataRow childRow = GetRowFromElement (args.Node as XmlElement); if (childRow != null) { // child might be a table row. // I might be impossible to set parent // since either of them might be detached if (childRow.RowState != DataRowState.Detached && row.RowState != DataRowState.Detached) { FillRelationship (row, childRow, args.NewParent, args.Node); } } else if (args.Node.NodeType == XmlNodeType.Element) { // child element might be a column DataColumn col = row.Table.Columns [args.Node.LocalName]; if (col != null) row [col] = StringToObject (col.DataType, args.Node.InnerText); } else if (args.Node is XmlCharacterData) { if (args.Node.NodeType != XmlNodeType.Comment) { for (int i = 0; i < row.Table.Columns.Count; i++) { DataColumn col = row.Table.Columns [i]; if (col.ColumnMapping == MappingType.SimpleContent) row [col] = StringToObject (col.DataType, args.Node.Value); } } } } } finally { raiseDataSetEvents = escapedRaiseDataSetEvents; } } private void CheckDescendantRelationship (XmlNode n) { XmlElement el = n as XmlElement; DataRow row = GetRowFromElement (el); if (row == null) return; row.Table.Rows.Add (row); // attach CheckDescendantRelationship (n, row); } private void CheckDescendantRelationship (XmlNode p, DataRow row) { foreach (XmlNode n in p.ChildNodes) { XmlElement el = n as XmlElement; if (el == null) continue; DataRow childRow = GetRowFromElement (el); if (childRow == null) continue; childRow.Table.Rows.Add (childRow); FillRelationship (row, childRow, p, el); } } private void FillRelationship (DataRow row, DataRow childRow, XmlNode parentNode, XmlNode childNode) { for (int i = 0; i < childRow.Table.ParentRelations.Count; i++) { DataRelation rel = childRow.Table.ParentRelations [i]; if (rel.ParentTable == row.Table) { childRow.SetParentRow (row); break; } } CheckDescendantRelationship (childNode, childRow); } #endregion // DataSet event handlers #region DataSet event handlers // If DataTable is added or removed from DataSet private void OnDataTableChanged (object sender, CollectionChangeEventArgs eventArgs) { if (!raiseDataSetEvents) return; bool escapedRaiseDocumentEvents = raiseDocumentEvents; raiseDocumentEvents = false; try { DataTable Table = (DataTable)eventArgs.Element; switch (eventArgs.Action) { case CollectionChangeAction.Add: Table.ColumnChanged += columnChanged; Table.RowDeleted += rowDeleted; Table.RowChanged += rowChanged; break; case CollectionChangeAction.Remove: Table.ColumnChanged -= columnChanged; Table.RowDeleted -= rowDeleted; Table.RowChanged -= rowChanged; break; } } finally { raiseDocumentEvents = escapedRaiseDocumentEvents; } } // If column has changed private void OnDataTableColumnChanged (object sender, DataColumnChangeEventArgs eventArgs) { if (!raiseDataSetEvents) return; bool escapedRaiseDocumentEvents = raiseDocumentEvents; raiseDocumentEvents = false; try { DataRow row = eventArgs.Row; XmlElement el = GetElementFromRow (row); if (el == null) return; DataColumn col = eventArgs.Column; string value = row.IsNull (col) ? String.Empty : row [col].ToString (); switch (col.ColumnMapping) { case MappingType.Attribute: el.SetAttribute (col.ColumnName, col.Namespace, value); break; case MappingType.SimpleContent: el.InnerText = value; break; case MappingType.Element: bool exists = false; for (int i = 0; i < el.ChildNodes.Count; i++) { XmlElement c = el.ChildNodes [i] as XmlElement; if (c != null && c.LocalName == col.ColumnName && c.NamespaceURI == col.Namespace) { exists = true; c.InnerText = value; break; } } if (!exists) { XmlElement cel = CreateElement (col.Prefix, col.ColumnName, col.Namespace); cel.InnerText = value; el.AppendChild (cel); } break; // FIXME: how to handle hidden? } } finally { raiseDocumentEvents = escapedRaiseDocumentEvents; } } private void OnDataTableRowDeleted (object sender, DataRowChangeEventArgs eventArgs) { if (!raiseDataSetEvents) return; bool escapedRaiseDocumentEvents = raiseDocumentEvents; raiseDocumentEvents = false; try { // This code is obsolete XmlDataDocument one DataRow deletedRow = null; deletedRow = eventArgs.Row; XmlElement el = GetElementFromRow (eventArgs.Row); if (el == null) return; el.ParentNode.RemoveChild (el); } finally { raiseDocumentEvents = escapedRaiseDocumentEvents; } } [MonoTODO ("Need to handle hidden columns? - see comments on each private method")] private void OnDataTableRowChanged (object sender, DataRowChangeEventArgs eventArgs) { if (!raiseDataSetEvents) return; bool escapedRaiseDocumentEvents = raiseDocumentEvents; raiseDocumentEvents = false; try { switch (eventArgs.Action) { case DataRowAction.Delete: OnDataTableRowDeleted (sender, eventArgs); break; case DataRowAction.Add: OnDataTableRowAdded (eventArgs); break; case DataRowAction.Rollback: OnDataTableRowRollback (eventArgs); break; default: break; } } finally { raiseDocumentEvents = escapedRaiseDocumentEvents; } } // Added - see FillNodeChildrenFromRow comment private void OnDataTableRowAdded (DataRowChangeEventArgs args) { if (!raiseDataSetEvents) return; bool escapedRaiseDocumentEvents = raiseDocumentEvents; raiseDocumentEvents = false; try { // Create row element. Row's name same as TableName DataRow row = args.Row; // create document element if it does not exist if (DocumentElement == null) this.AppendChild (CreateElement (DataSet.DataSetName)); DataTable table= args.Row.Table; XmlElement element = GetElementFromRow (row); if (element == null) element = CreateElement (table.Prefix, table.TableName, table.Namespace); if (element.ParentNode == null) { // parent is not always DocumentElement. XmlElement parent = null; if (table.ParentRelations.Count > 0) { for (int i = 0; i < table.ParentRelations.Count; i++) { DataRelation rel = table.ParentRelations [i]; DataRow parentRow = row.GetParentRow (rel); if (parentRow == null) continue; parent = GetElementFromRow (parentRow); } } // The row might be orphan. In such case, the // element is appended to DocumentElement. if (parent == null) parent = DocumentElement; parent.AppendChild (element); } } finally { raiseDocumentEvents = escapedRaiseDocumentEvents; } } private void FillNodeChildrenFromRow (DataRow row, XmlElement element) { DataTable table = row.Table; // fill columns for the row for (int i = 0; i < table.Columns.Count; i++) { DataColumn col = table.Columns [i]; string value = row.IsNull (col) ? String.Empty : row [col].ToString (); switch (col.ColumnMapping) { case MappingType.Element: XmlElement el = CreateElement (col.Prefix, col.ColumnName, col.Namespace); el.InnerText = value; element.AppendChild (el); break; case MappingType.Attribute: XmlAttribute attr = CreateAttribute (col.Prefix, col.ColumnName, col.Namespace); attr.Value = value; element.SetAttributeNode (attr); break; case MappingType.SimpleContent: XmlText text = CreateTextNode (value); element.AppendChild (text); break; // FIXME: how to handle hidden? } } } // Rollback [MonoTODO ("It does not look complete.")] private void OnDataTableRowRollback (DataRowChangeEventArgs args) { if (!raiseDataSetEvents) return; bool escapedRaiseDocumentEvents = raiseDocumentEvents; raiseDocumentEvents = false; try { DataRow r = args.Row; XmlElement el = GetElementFromRow (r); if (el == null) return; DataTable tab = r.Table; ArrayList al = new ArrayList (); foreach (XmlAttribute attr in el.Attributes) { DataColumn col = tab.Columns [attr.LocalName]; if (col != null) { if (r.IsNull (col)) // should be removed al.Add (attr); else attr.Value = r [col].ToString (); } } foreach (XmlAttribute attr in al) el.RemoveAttributeNode (attr); al.Clear (); foreach (XmlNode child in el.ChildNodes) { if (child.NodeType == XmlNodeType.Element) { DataColumn col = tab.Columns [child.LocalName]; if (col != null) { if (r.IsNull (col)) al.Add (child); else child.InnerText = r [col].ToString (); } } } foreach (XmlNode n in al) el.RemoveChild (n); } finally { raiseDocumentEvents = escapedRaiseDocumentEvents; } } #endregion // DataSet event handlers #region Private methods private void InitDelegateFields () { columnChanged = new DataColumnChangeEventHandler (OnDataTableColumnChanged); rowDeleted = new DataRowChangeEventHandler (OnDataTableRowDeleted); rowChanged = new DataRowChangeEventHandler (OnDataTableRowChanged); tablesChanged = new CollectionChangeEventHandler (OnDataTableChanged); } private void RemoveXmlDocumentListeners () { this.NodeInserting -= new XmlNodeChangedEventHandler (OnNodeInserting); this.NodeInserted -= new XmlNodeChangedEventHandler (OnNodeInserted); this.NodeChanging -= new XmlNodeChangedEventHandler (OnNodeChanging); this.NodeChanged -= new XmlNodeChangedEventHandler (OnNodeChanged); this.NodeRemoving -= new XmlNodeChangedEventHandler (OnNodeRemoving); this.NodeRemoved -= new XmlNodeChangedEventHandler (OnNodeRemoved); } private void AddXmlDocumentListeners () { this.NodeInserting += new XmlNodeChangedEventHandler (OnNodeInserting); this.NodeInserted += new XmlNodeChangedEventHandler (OnNodeInserted); this.NodeChanging += new XmlNodeChangedEventHandler (OnNodeChanging); this.NodeChanged += new XmlNodeChangedEventHandler (OnNodeChanged); this.NodeRemoving += new XmlNodeChangedEventHandler (OnNodeRemoving); this.NodeRemoved += new XmlNodeChangedEventHandler (OnNodeRemoved); } internal static object StringToObject (Type type, string value) { if (type == null) return value; switch (Type.GetTypeCode (type)) { case TypeCode.Boolean: return XmlConvert.ToBoolean (value); case TypeCode.Byte: return XmlConvert.ToByte (value); case TypeCode.Char: return (char)XmlConvert.ToInt32 (value); case TypeCode.DateTime: return XmlConvert.ToDateTime (value); case TypeCode.Decimal: return XmlConvert.ToDecimal (value); case TypeCode.Double: return XmlConvert.ToDouble (value); case TypeCode.Int16: return XmlConvert.ToInt16 (value); case TypeCode.Int32: return XmlConvert.ToInt32 (value); case TypeCode.Int64: return XmlConvert.ToInt64 (value); case TypeCode.SByte: return XmlConvert.ToSByte (value); case TypeCode.Single: return XmlConvert.ToSingle (value); case TypeCode.UInt16: return XmlConvert.ToUInt16 (value); case TypeCode.UInt32: return XmlConvert.ToUInt32 (value); case TypeCode.UInt64: return XmlConvert.ToUInt64 (value); } if (type == typeof (TimeSpan)) return XmlConvert.ToTimeSpan (value); if (type == typeof (Guid)) return XmlConvert.ToGuid (value); if (type == typeof (byte[])) return Convert.FromBase64String (value); return Convert.ChangeType (value, type); } #endregion // Private methods } }