File: Microsoft\Build\Tasks\Xaml\ClassImporter.cs
Project: ndp\cdf\src\NetFx40\XamlBuildTask\XamlBuildTask.csproj (XamlBuildTask)
//------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
//------------------------------------------------------------
 
namespace Microsoft.Build.Tasks.Xaml
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Globalization;
    using System.Linq;
    using System.Xaml;
    using System.Xaml.Schema;
    using System.ComponentModel;
    using System.Runtime;
    using System.Windows.Markup;
    using XamlBuildTask;
 
    class ClassImporter
    {
        bool DefaultClassIsPublic;
        MemberVisibility DefaultFieldVisibility;
        string xamlFileName;
        string localAssemblyName;
        string rootNamespace;
        NamespaceTable namespaceTable;
 
        public ClassImporter(string xamlFileName, string localAssemblyName, string rootNamespace)
        {
            this.xamlFileName = xamlFileName;
            this.localAssemblyName = localAssemblyName;
            this.rootNamespace = rootNamespace;
            this.DefaultClassIsPublic = true;
            this.DefaultFieldVisibility = MemberVisibility.Assembly;
            this.namespaceTable = new NamespaceTable(localAssemblyName);
        }
 
        // Throws InvalidOperationException at DesignTime: Input XAML contains invalid constructs for generating a class. For example, unexpected content or unknown class or field modifiers.
        public ClassData ReadFromXaml(XamlNodeList nodes)
        {
            if (nodes == null)
            {
                throw FxTrace.Exception.ArgumentNull("nodeList");
            }
 
            Stack<NamedObject> currentTypes = new Stack<NamedObject>();
            XamlReader reader = nodes.GetReader();
            XamlSchemaContext xsc = reader.SchemaContext;
            XamlNodeList strippedXamlNodes = new XamlNodeList(xsc);
            XamlWriter strippedXamlNodesWriter = strippedXamlNodes.Writer;
 
            ClassData result = new ClassData()
                {
                    FileName = this.xamlFileName,
                    IsPublic = this.DefaultClassIsPublic,
                    RootNamespace = this.rootNamespace
                };
 
            // We loop through the provided XAML; for each node, we do two things:
            //  1. If it's a directive that's relevant to x:Class, we extract the data.
            //  2. Unless it's a directive that's exclusively relevant to x:Class, we write it to strippedXamlNodes.
            // The result is two outputs: class data, and stripped XAML that can be used to initialize the
            // an instance of the class.
 
            bool readNextNode = false;
            while (readNextNode || reader.Read())
            {
                bool stripNodeFromXaml = false;
                readNextNode = false;
 
                namespaceTable.ManageNamespace(reader);
 
                switch (reader.NodeType)
                {
                    case XamlNodeType.StartObject:
                        if (result.BaseType == null)
                        {
                            result.BaseType = reader.Type;
                        }
                        currentTypes.Push(new NamedObject()
                            {
                                Type = reader.Type,
                                Visibility = DefaultFieldVisibility,
                            });
                        break;
 
                    case XamlNodeType.EndObject:
                        currentTypes.Pop();
                        break;
 
                    case XamlNodeType.StartMember:
                        XamlMember member = reader.Member;
 
                        if (member.IsDirective)
                        {
                            bool isRootElement = (currentTypes.Count == 1);
                            stripNodeFromXaml = ProcessDirective(reader, result, currentTypes.Peek(), isRootElement, strippedXamlNodes, out readNextNode);
                        }
                        else
                        {
                            NamedObject currentType = currentTypes.Peek();
                            XamlType currentXamlType = currentType.Type;
                            if (currentXamlType.IsUnknown)
                            {
                                result.RequiresCompilationPass2 = true;
                            }
                        }
                        break;
 
                    case XamlNodeType.EndMember:
                        break;
 
                    case XamlNodeType.Value:
                        break;
 
                    case XamlNodeType.NamespaceDeclaration:
                        break;
 
                    case XamlNodeType.None:
                        break;
 
                    case XamlNodeType.GetObject:
                        //Push a dummy NamedObject so that it gets popped when you see the corresponding EndObject
                        currentTypes.Push(new NamedObject());
                        break;
 
                    default:
 
                        Debug.Fail("Unrecognized XamlNodeType value" + reader.NodeType.ToString());
                        break;
                }
 
                if (!stripNodeFromXaml)
                {
                    WritestrippedXamlNode(reader, strippedXamlNodesWriter);
                }
            }
 
            // ClassData.Name should be initialized to a non-null non-empty value if 
            // the file contains x:Class. Throw an error if neither is found.
            if (result.Name == null)
            {
                string xClassDirectiveName = "{" + XamlLanguage.Class.PreferredXamlNamespace + "}" + XamlLanguage.Class.Name;
 
                throw FxTrace.Exception.AsError(LogInvalidOperationException(null, SR.TaskCannotProcessFileWithoutType(xClassDirectiveName)));
            }
 
            strippedXamlNodes.Writer.Close();
            strippedXamlNodes = RewriteRootNode(strippedXamlNodes, result.Name, result.Namespace);
 
            result.EmbeddedResourceXaml = strippedXamlNodes;
            return result;
        }
 
        IList<XamlType> UpdateTypeArgs(IList<XamlType> typeArgs, XamlSchemaContext xsc)
        {
            if (typeArgs != null)
            {
                IList<XamlType> updatedTypeArgs = new List<XamlType>();
                foreach (var typeArg in typeArgs)
                {
                    IList<XamlType> typeArgTypeArgs = UpdateTypeArgs(typeArg.TypeArguments, xsc);
                    string typeArgXmlns = XamlBuildTaskServices.UpdateClrNamespaceUriWithLocalAssembly(typeArg.PreferredXamlNamespace, this.localAssemblyName);
                    updatedTypeArgs.Add(new XamlType(typeArgXmlns, typeArg.Name, typeArgTypeArgs, xsc));
                }
                return updatedTypeArgs;
            }
            return typeArgs;
        }
 
        void WritestrippedXamlNode(XamlReader reader, XamlWriter writer)
        {
            switch (reader.NodeType)
            {
                case XamlNodeType.StartObject:
                    XamlType xamlType = reader.Type;
                    if (xamlType.IsUnknown)
                    {
                        IList<XamlType> typeArgs = UpdateTypeArgs(xamlType.TypeArguments, reader.SchemaContext);
                        string xmlns = XamlBuildTaskServices.UpdateClrNamespaceUriWithLocalAssembly(xamlType.PreferredXamlNamespace, this.localAssemblyName);
                        xamlType = new XamlType(xmlns, xamlType.Name, typeArgs, reader.SchemaContext);
                    }
                    writer.WriteStartObject(xamlType);
                    break;
 
                case XamlNodeType.StartMember:
                    XamlMember member = reader.Member;
                    if (member.IsUnknown && !member.IsDirective)
                    {
                        string xmlns = XamlBuildTaskServices.UpdateClrNamespaceUriWithLocalAssembly(member.DeclaringType.PreferredXamlNamespace, this.localAssemblyName);
                        XamlType memberXamlType = new XamlType(xmlns, member.DeclaringType.Name, member.DeclaringType.TypeArguments, reader.SchemaContext);
                        member = new XamlMember(member.Name, memberXamlType, member.IsAttachable);
                    }
                    writer.WriteStartMember(member);
                    break;
 
                case XamlNodeType.NamespaceDeclaration:
                    NamespaceDeclaration ns = new NamespaceDeclaration(
                        XamlBuildTaskServices.UpdateClrNamespaceUriWithLocalAssembly(reader.Namespace.Namespace, this.localAssemblyName),
                        reader.Namespace.Prefix);
                    writer.WriteNamespace(ns);
                    break;
 
                case XamlNodeType.GetObject:
                case XamlNodeType.EndObject:
                case XamlNodeType.EndMember:
                case XamlNodeType.Value:
                case XamlNodeType.None:
                    writer.WriteNode(reader);
                    break;
 
                default:
                    Debug.Fail("Unrecognized XamlNodeType value" + reader.NodeType.ToString());
                    break;
            }
        }
 
        XamlNodeList RewriteRootNode(XamlNodeList strippedXamlNodes, string name, string @namespace)
        {
            // Rewrite the root node to have the name of class declared via x:Class (rather than the base class)
            // Also, for any properties on the root object that are declared in this class, need to rewrite the
            // namespace to include the root namespace, if there is one.
 
            string oldNamespace = null;
            if (!string.IsNullOrEmpty(this.rootNamespace))
            {
                oldNamespace = @namespace;
                if (!string.IsNullOrEmpty(@namespace))
                {
                    @namespace = this.rootNamespace + "." + @namespace;
                }
                else
                {
                    @namespace = this.rootNamespace;
                }
            }
 
            string namespaceName = string.Format(CultureInfo.InvariantCulture, "{0}{1};{2}{3}", XamlBuildTaskServices.ClrNamespaceUriNamespacePart, @namespace, XamlBuildTaskServices.ClrNamespaceUriAssemblyPart, this.localAssemblyName);
 
            XamlReader reader = strippedXamlNodes.GetReader();
            XamlSchemaContext xsc = reader.SchemaContext;
            XamlNodeList newStrippedXamlNodes = new XamlNodeList(xsc);
            XamlWriter writer = newStrippedXamlNodes.Writer;
 
            int depth = 0;
            XamlType rootXamlType = null;
            while (reader.Read())
            {
                switch (reader.NodeType)
                {
                    case XamlNodeType.StartObject:
                    case XamlNodeType.GetObject:
                        depth++;
                        break;
                    case XamlNodeType.EndObject:
                        depth--;
                        break;
                }
                if (reader.NodeType == XamlNodeType.StartObject && depth == 1)
                {
                    rootXamlType = new XamlType(namespaceName, name, null, xsc);
                    writer.WriteStartObject(rootXamlType);
                }
                else if (reader.NodeType == XamlNodeType.StartMember && depth == 1 && reader.Member.IsUnknown 
                    && reader.Member.DeclaringType != null && reader.Member.DeclaringType.Name == rootXamlType.Name)
                {
                    string clrNs;
                    XamlMember member = reader.Member;
                    if (XamlBuildTaskServices.TryExtractClrNs(member.PreferredXamlNamespace, out clrNs) &&
                        clrNs == oldNamespace)
                    {
                        // This is a member defined on the document root type, but missing the project root namespace. Fix it.
                        XamlMember newMember = new XamlMember(member.Name, rootXamlType, member.IsAttachable);
                        Fx.Assert(rootXamlType != null, "First StartObject should already have been processed");
                        writer.WriteStartMember(newMember);
                    }
                    else
                    {
                        writer.WriteNode(reader);
                    }
                }
                else
                {
                    writer.WriteNode(reader);
                }
            }
 
            writer.Close();
            return newStrippedXamlNodes;
        }
 
        bool ProcessDirective(XamlReader reader, ClassData classData,
            NamedObject currentObject, bool isRootElement, XamlNodeList strippedXamlNodes, out bool readNextNode)
        {
            Fx.Assert(reader.NodeType == XamlNodeType.StartMember, "Current node should be a Start Member Node");
 
            XamlMember member = reader.Member;
            bool directiveRecognized = false;
            readNextNode = false;
 
            switch (member.Name)
            {
                case "Name":
                    // Unlike all the other directives that we process, x:Name should be written
                    // to the stripped output.
                    strippedXamlNodes.Writer.WriteStartMember(member);
 
                    string objectName = ReadAtom(reader, XamlLanguage.Name.Name);
                    if (!objectName.StartsWith(XamlBuildTaskServices.SerializerReferenceNamePrefix,
                        StringComparison.Ordinal))
                    {
                        currentObject.Name = objectName;
                        classData.NamedObjects.Add(currentObject);
                    }
 
                    strippedXamlNodes.Writer.WriteValue(objectName);
                    strippedXamlNodes.Writer.WriteEndMember();
                    directiveRecognized = true;
                    break;
 
                case "Class":
                    if (isRootElement)
                    {
                        string fullClassName = ReadAtom(reader, XamlLanguage.Class.Name);
                        SetClassName(fullClassName, classData);
                        directiveRecognized = true;
                    }
                    break;
 
                case "ClassModifier":
                    if (isRootElement)
                    {
                        string classModifier = ReadAtom(reader, XamlLanguage.ClassModifier.Name);
                        classData.IsPublic = XamlBuildTaskServices.IsPublic(classModifier);
                        directiveRecognized = true;
                    }
                    break;
 
                case "FieldModifier":
                    string fieldModifier = ReadAtom(reader, XamlLanguage.FieldModifier.Name);
                    currentObject.Visibility = XamlBuildTaskServices.GetMemberVisibility(fieldModifier);
                    directiveRecognized = true;
                    break;
 
                case "Code":
                    string codeSnippet = ReadAtom(reader, XamlLanguage.Code.Name);
                    classData.CodeSnippets.Add(codeSnippet);
                    directiveRecognized = true;
                    break;
 
                case "Members":
                    foreach (PropertyData property in ReadProperties(reader.ReadSubtree()))
                    {
                        classData.Properties.Add(property);
                    }
                    if (!classData.RequiresCompilationPass2)
                    {
                        foreach (PropertyData property in classData.Properties)
                        {
                            if (property.Type.IsUnknown)
                            {
                                classData.RequiresCompilationPass2 = true;
                                break;
                            }
                        }
                    }
                    directiveRecognized = true;
                    readNextNode = true;
                    break;
 
                case "ClassAttributes":
                    foreach (AttributeData attribute in ReadAttributesCollection(reader.ReadSubtree()))
                    {
                        classData.Attributes.Add(attribute);
                    }
                    directiveRecognized = true;
                    readNextNode = true;
                    break;
 
            }
 
            if (directiveRecognized == true && readNextNode == false)
            {
                reader.Read();
                Fx.Assert(reader.NodeType == XamlNodeType.EndMember, "Current node should be a XamlEndmember");
            }
 
            return directiveRecognized;
        }
 
        private IList<AttributeData> ReadAttributesCollection(XamlReader reader)
        {
            IList<AttributeData> attributes = new List<AttributeData>();
            bool nextNodeRead = false;
            while (nextNodeRead || reader.Read())
            {
                this.namespaceTable.ManageNamespace(reader);
                nextNodeRead = false;
                if (reader.NodeType == XamlNodeType.StartObject && reader.Type != null)
                {
                    AttributeData attribute = null;
                    try
                    {
                        attribute = AttributeData.LoadAttributeData(reader.ReadSubtree(), this.namespaceTable, this.rootNamespace);
                    }
                    catch (InvalidOperationException e)
                    {
                        throw FxTrace.Exception.AsError(LogInvalidOperationException(reader, e.Message));
                    }
                    nextNodeRead = true;
                    attributes.Add(attribute);
                }
            }
 
            return attributes;
        }
 
        IEnumerable<PropertyData> ReadProperties(XamlReader reader)
        {
            IDictionary<string, PropertyData> members = new Dictionary<string, PropertyData>();
 
            bool nextNodeRead = false;
            while (nextNodeRead || reader.Read())
            {
                namespaceTable.ManageNamespace(reader);
                nextNodeRead = false;
                if (reader.NodeType == XamlNodeType.StartObject)
                {
                    if (reader.Type == XamlLanguage.Property)
                    {
                        PropertyData xProperty = LoadProperty(reader.ReadSubtree());
                        nextNodeRead = true;
                        if (members.ContainsKey(xProperty.Name))
                        {
                            throw FxTrace.Exception.AsError(LogInvalidOperationException(reader, SR.DuplicatePropertyDefinition(xProperty.Name)));
                        }
                        members.Add(xProperty.Name, xProperty);
                    }
                }
            }
 
            return members.Values;
        }
 
        PropertyData LoadProperty(XamlReader xamlReader)
        {
            if (xamlReader == null)
            {
                throw FxTrace.Exception.ArgumentNull("xamlReader");
            }
 
            PropertyData property = new PropertyData();
            while (xamlReader.Read())
            {
                if (xamlReader.NodeType == XamlNodeType.StartMember)
                {
                    XamlMember member = xamlReader.Member;
                    switch (member.Name)
                    {
                        case "Name":
                            property.Name = ReadValueAsString(xamlReader.ReadSubtree());
                            break;
                        case "Type":
                            property.Type = ReadPropertyType(xamlReader.ReadSubtree());
                            break;
                        case "Attributes":
                            foreach (AttributeData attribute in ReadAttributesCollection(xamlReader.ReadSubtree()))
                            {
                                property.Attributes.Add(attribute);
                            }
                            break;
                        case "Modifier":
                            string propertyModifier = ReadValueAsString(xamlReader.ReadSubtree());
                            property.Visibility = XamlBuildTaskServices.GetMemberVisibility(propertyModifier);
                            break;
                        default:
                            // Ignore AttachedProperties on property
                            if (!member.IsAttachable)
                            {
                                throw FxTrace.Exception.AsError(LogInvalidOperationException(xamlReader, SR.UnknownPropertyMember(member.Name)));
                            }                            
                            break;
                    }
                }
            }
            if (string.IsNullOrEmpty(property.Name))
            {
                throw FxTrace.Exception.AsError(LogInvalidOperationException(xamlReader, SR.PropertyNameRequired));
            }
            if (property.Type == null)
            {
                throw FxTrace.Exception.AsError(LogInvalidOperationException(xamlReader, SR.PropertyTypeRequired(property.Name)));
            }
            return property;
        }
 
        XamlType ReadPropertyType(XamlReader xamlReader)
        {
            while (xamlReader.Read())
            {
                if (xamlReader.NodeType == XamlNodeType.Value && xamlReader.Value is string)
                {
                    return XamlBuildTaskServices.GetXamlTypeFromString((string)xamlReader.Value, this.namespaceTable, xamlReader.SchemaContext);
                }
            }
            return null;
        }
 
        string ReadValueAsString(XamlReader xamlReader)
        {
            while (xamlReader.Read())
            {
                if (xamlReader.NodeType == XamlNodeType.Value)
                {
                    return xamlReader.Value as string;
                }
            }
            return string.Empty;
        }
 
        string ReadAtom(XamlReader reader, string propertyName)
        {
            reader.Read();
            if (reader.NodeType != XamlNodeType.Value)
            {
                throw FxTrace.Exception.AsError(LogInvalidOperationException(reader, SR.TextRepresentationExpected(propertyName)));
            }
            return (string)reader.Value;
        }
 
        void SetClassName(string fullClassName, ClassData classData)
        {
            int lastIndex = fullClassName.LastIndexOf('.');
            if (lastIndex != -1)
            {
                string classNamespace = fullClassName.Substring(0, lastIndex);
                string className = fullClassName.Substring(lastIndex + 1);
 
                classData.Name = className;
                classData.Namespace = classNamespace;
            }
            else
            {
                classData.Name = fullClassName;
                classData.Namespace = String.Empty;
            }
 
            if (string.IsNullOrEmpty(classData.Name))
            {
                throw FxTrace.Exception.AsError(LogInvalidOperationException(null, SR.ClassNameMustBeNonEmpty));
            }
        }
 
        Exception LogInvalidOperationException(XamlReader reader, string exceptionMessage)
        {
            IXamlLineInfo lineInfo = reader == null ? null : reader as IXamlLineInfo;
            if (lineInfo != null && lineInfo.HasLineInfo)
            {
                return new LoggableException(new InvalidOperationException(exceptionMessage))
                {
                    Source = this.xamlFileName,
                    LineNumber = lineInfo.LineNumber,
                    LinePosition = lineInfo.LinePosition
                };
            }
            else
            {
                return new LoggableException(new InvalidOperationException(exceptionMessage))
                {
                    Source = this.xamlFileName
                };
            }
        }       
    }
}