#region Using Statements using ANX.Framework.NonXNA.Development; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Xml; #endregion // This file is part of the ANX.Framework created by the // "ANX.Framework developer group" and released under the Ms-PL license. // For details see: http://anxframework.codeplex.com/license namespace ANX.Framework.Content.Pipeline.Serialization.Intermediate { /// /// Provides an implementation of many of the methods of IntermediateSerializer including serialization and state tracking for shared resources and external references. /// [Developer("KorsarNek")] [TestState(TestStateAttribute.TestState.Tested)] [PercentageComplete(100)] public sealed class IntermediateWriter { private struct ExternalReference { public Type TargetType; public string Filename; public string ID; } private struct RecursionIdentifier { public object instance; public Type targetType; } private string basePath; private List writtenTypes = new List(); private Dictionary recurseDetector = new Dictionary(); private Dictionary sharedResourceNames = new Dictionary(new ReferenceEqualityComparer()); private Queue sharedResources = new Queue(); private List externalReferences = new List(); private IntermediateXmlWriter realXml; private StringBuilder inMemoryXml = new StringBuilder(); private XmlTypeNameWriter xmlTypeNameWriter; /// /// Gets the parent serializer. /// public IntermediateSerializer Serializer { get; private set; } /// /// Gets the XML output stream. /// public IntermediateXmlWriter Xml { get; private set; } internal IntermediateWriter(IntermediateSerializer serializer, IntermediateXmlWriter xmlWriter, string basePath, XmlTypeNameContainer xmlTypeNameContainer, bool shortenNamespaces) { this.Serializer = serializer; this.realXml = xmlWriter; this.xmlTypeNameWriter = new XmlTypeNameWriter(xmlTypeNameContainer, shortenNamespaces); StringWriter memoryWriter = new StringWriter(inMemoryXml); //The xml declaration will already be contained in the given xmlWriter. this.Xml = new IntermediateXmlWriter(XmlWriter.Create(memoryWriter, new XmlWriterSettings() { OmitXmlDeclaration = true, ConformanceLevel = ConformanceLevel.Auto })); //We need a full path to be able to make the compiled files relative to it. if (basePath != null) { basePath = Path.GetFullPath(basePath); } this.basePath = basePath; } /// /// Writes a single object to the output XML stream. /// /// /// The value to write. /// The format of the XML. public void WriteObject(T value, ContentSerializerAttribute format) { this.WriteObject(value, format, this.Serializer.GetTypeSerializer(typeof(T))); } /// /// Writes a single object to the output XML stream, using the specified type hint. /// /// /// The value to write. /// The format of the XML. /// The type serializer. public void WriteObject(T value, ContentSerializerAttribute format, ContentTypeSerializer typeSerializer) { if (typeSerializer == null) throw new ArgumentNullException("typeSerializer"); if (format == null) { throw new ArgumentNullException("format"); } if (!format.FlattenContent) { if (string.IsNullOrEmpty(format.ElementName)) throw new ArgumentException("ContentSerializerAttribute has a null ElementName property."); this.Xml.WriteStartElement(format.ElementName); } if (value == null) { if (format.FlattenContent) throw new InvalidOperationException("Cannot serialize null values when the ContentSerializerAttribute.FlattenContent flag is set."); this.Xml.WriteAttributeString("Null", "true"); } else { Type type = value.GetType(); if (type.IsSubclassOf(typeof(Type))) { type = typeof(Type); } //Special handling for nullables. Reason is that calling GetType() on a nullable will return the underlying type, not the nullable, therefore we would //wrongly write the underlying type into the xml even though it is already known. Type targetType = typeSerializer.TargetType; if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Nullable<>)) { targetType = Nullable.GetUnderlyingType(targetType); } if (type != targetType) { if (format.FlattenContent) throw new InvalidOperationException("Cannot serialize derived types when the ContentSerializerAttribute.FlattenContent flag is set."); typeSerializer = this.Serializer.GetTypeSerializer(type); this.Xml.WriteStartAttribute("Type"); this.WriteTypeName(typeSerializer.TargetType); this.Xml.WriteEndAttribute(); } RecursionIdentifier recursion = new RecursionIdentifier() { instance = value, targetType = typeof(T) }; if (this.recurseDetector.ContainsKey(recursion)) throw new InvalidOperationException(string.Format("Cyclic reference found while serializing {0}. You may be missing a ContentSerializerAttribute.SharedResource flag.", value)); this.recurseDetector.Add(recursion, true); typeSerializer.Serialize(this, value, format); this.recurseDetector.Remove(recursion); writtenTypes.Add(typeSerializer.TargetType); } if (!format.FlattenContent) { this.Xml.WriteEndElement(); } } /// /// Writes a single object to the output XML stream using the specified serializer worker. /// /// /// The value to write. /// The format of the XML. public void WriteRawObject(T value, ContentSerializerAttribute format) { if (value == null) throw new ArgumentException("value"); this.WriteRawObject(value, format, this.Serializer.GetTypeSerializer(value.GetType())); } /// /// Writes a single object to the output XML stream as an instance of the specified type. /// /// /// The value to write. /// The format of the XML. /// The type serializer. public void WriteRawObject(T value, ContentSerializerAttribute format, ContentTypeSerializer typeSerializer) { if (value == null) throw new ArgumentException("value"); if (typeSerializer == null) throw new ArgumentNullException("typeSerializer"); if (format == null) { throw new ArgumentNullException("format"); } if (!format.FlattenContent) { if (string.IsNullOrEmpty(format.ElementName)) { throw new ArgumentException("\"format\" must have an ElementName if FlattenContent is false."); } this.Xml.WriteStartElement(format.ElementName); } typeSerializer.Serialize(this, value, format); if (!format.FlattenContent) { this.Xml.WriteEndElement(); } } /// /// Adds a shared reference to the output XML and records the object to be serialized later. /// /// /// The value to write. /// The format of the XML. public void WriteSharedResource(T value, ContentSerializerAttribute format) { if (format == null) throw new ArgumentNullException("format"); if (!format.FlattenContent) { if (string.IsNullOrEmpty(format.ElementName)) throw new ArgumentException("ContentSerializerAttribute has a null ElementName property."); this.Xml.WriteStartElement(format.ElementName); } if (value != null) { string text; if (!this.sharedResourceNames.TryGetValue(value, out text)) { text = "#Resource" + (this.sharedResourceNames.Count + 1).ToString(CultureInfo.InvariantCulture); this.sharedResourceNames.Add(value, text); this.sharedResources.Enqueue(value); } this.Xml.WriteString(text); } if (!format.FlattenContent) { this.Xml.WriteEndElement(); } } internal void WriteSharedResources() { if (this.sharedResources.Count > 0) { this.Xml.WriteStartElement("Resources"); ContentSerializerAttribute contentSerializerAttribute = new ContentSerializerAttribute(); contentSerializerAttribute.ElementName = "Resource"; contentSerializerAttribute.FlattenContent = true; while (this.sharedResources.Count > 0) { object obj = this.sharedResources.Dequeue(); Type type = obj.GetType(); ContentTypeSerializer typeSerializer = this.Serializer.GetTypeSerializer(type); this.Xml.WriteStartElement("Resource"); this.Xml.WriteAttributeString("ID", this.sharedResourceNames[obj]); this.Xml.WriteStartAttribute("Type"); this.WriteTypeName(type); this.Xml.WriteEndAttribute(); this.WriteRawObject(obj, contentSerializerAttribute, typeSerializer); this.Xml.WriteEndElement(); } this.Xml.WriteEndElement(); } } /// /// Adds an external reference to the output XML, and records the filename to be serialized later. /// /// /// The external reference to add. public void WriteExternalReference(ExternalReference value) { if (value == null || value.Filename == null) { return; } //TODO: It's currently not possible to reference files that are a directory higher, only that are lower than the current file or at the same level. //Should think about making these paths relative with URIs which would allow reference files that are higher, but would always make a relative path if a basePath is given. string relativePath = value.Filename; if (relativePath != null && basePath != null && relativePath.StartsWith(basePath)) { relativePath = relativePath.Substring(basePath.Length).Trim(Path.DirectorySeparatorChar); } string id = null; foreach (IntermediateWriter.ExternalReference current in this.externalReferences) { if (current.TargetType == typeof(T) && current.Filename == relativePath) { id = current.ID; break; } } if (id == null) { id = "#External" + (this.externalReferences.Count + 1).ToString(CultureInfo.InvariantCulture); IntermediateWriter.ExternalReference item; item.TargetType = typeof(T); item.Filename = relativePath; item.ID = id; this.externalReferences.Add(item); } this.Xml.WriteElementString("Reference", id); } internal void WriteExternalReferences() { if (this.externalReferences.Count > 0) { this.Xml.WriteStartElement("ExternalReferences"); foreach (IntermediateWriter.ExternalReference current in this.externalReferences) { this.Xml.WriteStartElement("ExternalReference"); this.Xml.WriteAttributeString("ID", current.ID); this.Xml.WriteStartAttribute("TargetType"); this.WriteTypeName(current.TargetType); this.Xml.WriteEndAttribute(); this.Xml.WriteString(current.Filename); this.Xml.WriteEndElement(); } this.Xml.WriteEndElement(); } } internal void WriteUsedNamespaces() { foreach (var xmlNamespace in this.xmlTypeNameWriter.NamespaceAbbreviations) { this.realXml.WriteAttributeString("xmlns", xmlNamespace.Value, null, xmlNamespace.Key); } } internal void ComposeXml() { this.Xml.Flush(); this.realXml.WriteRaw(this.inMemoryXml.ToString()); } /// /// Writes a managed type descriptor to the XML output stream. /// /// The type. public void WriteTypeName(Type type) { if (type == null) throw new ArgumentNullException("type"); this.Xml.WriteString(this.xmlTypeNameWriter.getXmlTypeName(type)); } } }