File: System\Data\EntityModel\SchemaObjectModel\SchemaManager.cs
Project: ndp\fx\src\DataEntity\System.Data.Entity.csproj (System.Data.Entity)
//---------------------------------------------------------------------
// <copyright file="SchemaManager.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
//
// @owner       Microsoft
// @backupOwner Microsoft
//---------------------------------------------------------------------
 
using System.Collections;
using System.Collections.Generic;
using System.Data.Common;
using System.Data.Common.Utils;
using System.Data.Entity;
using System.Data.Mapping;
using System.Data.Metadata.Edm;
using System.Diagnostics;
using System.Linq;
using System.Xml;
 
namespace System.Data.EntityModel.SchemaObjectModel
{
    internal delegate void AttributeValueNotification(string token, Action<string, ErrorCode, EdmSchemaErrorSeverity> addError);
    internal delegate DbProviderManifest ProviderManifestNeeded(Action<string, ErrorCode, EdmSchemaErrorSeverity> addError);
 
    /// <summary>
    /// Class responsible for parsing,validating a collection of schema
    /// </summary>
    [System.Diagnostics.DebuggerDisplay("DataModel={DataModel}")]
    internal class SchemaManager
    {
        #region Instance Fields
        // This keeps track of all the possible namespaces encountered till now. This helps in displaying the error to the
        // user - if the particular type is not found, we can report whether the namespace was invalid or the type with the
        // given name was not found in the given namespace. This also helps in validating the namespace in the using elements
        private readonly HashSet<string> _namespaceLookUpTable = new HashSet<string>(StringComparer.Ordinal);
 
        // List of all the schema types across all the schemas. This is to ensure that there is no duplicate type encountered
        // across schemas
        private readonly SchemaElementLookUpTable<SchemaType> _schemaTypes = new SchemaElementLookUpTable<SchemaType>();
 
        // We want to stop parsing/resolving/validation after the first 100 errors
        private const int MaxErrorCount = 100;
 
        // delay loaded
        private DbProviderManifest _providerManifest;
        private PrimitiveSchema _primitiveSchema;
        private double effectiveSchemaVersion = XmlConstants.UndefinedVersion;
 
        private readonly SchemaDataModelOption _dataModel;
        private readonly ProviderManifestNeeded _providerManifestNeeded;
        private readonly AttributeValueNotification _providerNotification;
        private readonly AttributeValueNotification _providerManifestTokenNotification;
        #endregion
 
        #region Constructor
        private SchemaManager(SchemaDataModelOption dataModel, AttributeValueNotification providerNotification, AttributeValueNotification providerManifestTokenNotification, ProviderManifestNeeded providerManifestNeeded)
        {
            _dataModel = dataModel;
            _providerNotification = providerNotification;
            _providerManifestTokenNotification = providerManifestTokenNotification;
            _providerManifestNeeded = providerManifestNeeded;
        }
        #endregion
 
        #region Public Methods
 
 
        
        public static IList<EdmSchemaError> LoadProviderManifest(XmlReader xmlReader, string location,
            bool checkForSystemNamespace, out Schema schema)
        {
            IList<Schema> schemaCollection = new List<Schema>(1);
 
            DbProviderManifest providerManifest = checkForSystemNamespace ? EdmProviderManifest.Instance : null;
            IList<EdmSchemaError> errors = ParseAndValidate(new XmlReader[] { xmlReader },
                new string[] { location }, SchemaDataModelOption.ProviderManifestModel,
                providerManifest, out schemaCollection);
 
            // In case of errors, there are no schema in the schema collection
            if (schemaCollection.Count != 0)
            {
                schema = schemaCollection[0];
            }
            else
            {
                Debug.Assert(errors.Count != 0, "There must be some error encountered");
                schema = null;
            }
 
            return errors;
        }
 
        public static void NoOpAttributeValueNotification(string attributeValue, Action<string, ErrorCode, EdmSchemaErrorSeverity> addError) { }
        
        public static IList<EdmSchemaError> ParseAndValidate(IEnumerable<XmlReader> xmlReaders,
            IEnumerable<string> sourceFilePaths, SchemaDataModelOption dataModel,
            DbProviderManifest providerManifest,
            out IList<Schema> schemaCollection)
        {
            return ParseAndValidate(xmlReaders, 
                sourceFilePaths, 
                dataModel,
                NoOpAttributeValueNotification,
                NoOpAttributeValueNotification,
                delegate(Action<string, ErrorCode, EdmSchemaErrorSeverity> addError) { return providerManifest == null ? MetadataItem.EdmProviderManifest : providerManifest; },
                out schemaCollection);
        }
 
        public static IList<EdmSchemaError> ParseAndValidate(IEnumerable<XmlReader> xmlReaders,
            IEnumerable<string> sourceFilePaths, SchemaDataModelOption dataModel,
            AttributeValueNotification providerNotification,
            AttributeValueNotification providerManifestTokenNotification,
            ProviderManifestNeeded providerManifestNeeded,
            out IList<Schema> schemaCollection)
        {
            SchemaManager schemaManager = new SchemaManager(dataModel, providerNotification, providerManifestTokenNotification, providerManifestNeeded);
            var errorCollection = new List<EdmSchemaError>();
            schemaCollection = new List<Schema>();
            bool errorEncountered = false;
 
            List<string> filePathList;
            if (sourceFilePaths != null)
            {
                filePathList = new List<string>(sourceFilePaths);
            }
            else
            {
                filePathList = new List<string>();
            }
 
            int index = 0;
            foreach (XmlReader xmlReader in xmlReaders)
            {
                string location = null;
                if (filePathList.Count <= index)
                {
                    TryGetBaseUri(xmlReader, out location);
                }
                else
                {
                    location = filePathList[index];
                }
 
                Schema schema;
                schema = new Schema(schemaManager);
 
                var errorsForCurrentSchema = schema.Parse(xmlReader, location);
 
                CheckIsSameVersion(schema, schemaCollection, errorCollection);
                
                // If the number of errors exceeded the max error count, then return
                if (UpdateErrorCollectionAndCheckForMaxErrors(errorCollection, errorsForCurrentSchema, ref errorEncountered))
                {
                    return errorCollection;
                }
 
                // Add the schema to the collection if there are no errors. There are errors in which schema do not have any namespace.
                // Also if there is an error encountered in one of the schema, we do not need to add the remaining schemas since
                // we will never go to the resolve phase.
                if (!errorEncountered)
                {
                    schemaCollection.Add(schema);
                    schemaManager.AddSchema(schema);
                    var currentSchemaVersion = schema.SchemaVersion;
                    Debug.Assert(schemaCollection.All(s => s.SchemaVersion == currentSchemaVersion || s.SchemaVersion != XmlConstants.UndefinedVersion));
                }
                index++;
            }
 
            // If there are no errors encountered in the parsing stage, we can proceed to the 
            // parsing and validating phase
            if (!errorEncountered)
            {
                foreach (Schema schema in schemaCollection)
                {
                    if (UpdateErrorCollectionAndCheckForMaxErrors(errorCollection, schema.Resolve(), ref errorEncountered))
                    {
                        return errorCollection;
                    }
                }
 
                // If there are no errors encountered in the parsing stage, we can proceed to the 
                // parsing and validating phase
                if (!errorEncountered)
                {
                    foreach (Schema schema in schemaCollection)
                    {
                        if (UpdateErrorCollectionAndCheckForMaxErrors(errorCollection, schema.ValidateSchema(), ref errorEncountered))
                        {
                            return errorCollection;
                        }
                    }
                }
            }
            
            return errorCollection;
        }
 
        // this method will move skip down to the first element, or to the end if it doesn't find one
        internal static bool TryGetSchemaVersion(XmlReader reader, out double version, out DataSpace dataSpace)
        {
            // to make life simpler, we skip down to the first/root element, unless we're
            // already there
            if (!reader.EOF && reader.NodeType != XmlNodeType.Element)
            {
                while (reader.Read() && reader.NodeType != XmlNodeType.Element)
                {
                }
            }
 
            if (!reader.EOF &&
                (reader.LocalName == XmlConstants.Schema || reader.LocalName == StorageMslConstructs.MappingElement))
            {
                return TryGetSchemaVersion(reader.NamespaceURI, out version, out dataSpace);
            }
 
            version = default(double);
            dataSpace = default(DataSpace);
            return false;
        }
 
        internal static bool TryGetSchemaVersion(string xmlNamespaceName, out double version, out DataSpace dataSpace)
        {
            switch (xmlNamespaceName)
            {
                case XmlConstants.ModelNamespace_1:
                    version = XmlConstants.EdmVersionForV1;
                    dataSpace = DataSpace.CSpace;
                    return true;
                case XmlConstants.ModelNamespace_1_1:
                    version = XmlConstants.EdmVersionForV1_1;
                    dataSpace = DataSpace.CSpace;
                    return true;
                case XmlConstants.ModelNamespace_2:
                    version = XmlConstants.EdmVersionForV2;
                    dataSpace = DataSpace.CSpace;
                    return true;
                case XmlConstants.ModelNamespace_3:
                    version = XmlConstants.EdmVersionForV3;
                    dataSpace = DataSpace.CSpace;
                    return true;
                case XmlConstants.TargetNamespace_1:
                    version = XmlConstants.StoreVersionForV1;
                    dataSpace = DataSpace.SSpace;
                    return true;
                case XmlConstants.TargetNamespace_2:
                    version = XmlConstants.StoreVersionForV2;
                    dataSpace = DataSpace.SSpace;
                    return true;
                case XmlConstants.TargetNamespace_3:
                    version = XmlConstants.StoreVersionForV3;
                    dataSpace = DataSpace.SSpace;
                    return true;
                case StorageMslConstructs.NamespaceUriV1:
                    version = StorageMslConstructs.MappingVersionV1;
                    dataSpace = DataSpace.CSSpace;
                    return true;
                case StorageMslConstructs.NamespaceUriV2:
                    version = StorageMslConstructs.MappingVersionV2;
                    dataSpace = DataSpace.CSSpace;
                    return true;
                case StorageMslConstructs.NamespaceUriV3:
                    version = StorageMslConstructs.MappingVersionV3;
                    dataSpace = DataSpace.CSSpace;
                    return true;
                default:
                    version = default(Double);
                    dataSpace = default(DataSpace);
                    return false;
            }
        }
 
        private static bool CheckIsSameVersion(Schema schemaToBeAdded, IEnumerable<Schema> schemaCollection, List<EdmSchemaError> errorCollection)
        {
            if (schemaToBeAdded.SchemaVersion != XmlConstants.UndefinedVersion && schemaCollection.Count() > 0)
            {
                if (schemaCollection.Any(s => s.SchemaVersion != XmlConstants.UndefinedVersion && s.SchemaVersion != schemaToBeAdded.SchemaVersion))
                {
                    errorCollection.Add(
                    new EdmSchemaError(
                        Strings.CannotLoadDifferentVersionOfSchemaInTheSameItemCollection,
                        (int)ErrorCode.CannotLoadDifferentVersionOfSchemaInTheSameItemCollection,
                        EdmSchemaErrorSeverity.Error));
                }
            }
            return true;
        }
 
        public double SchemaVersion { get { return this.effectiveSchemaVersion; } }
 
        /// <summary>
        /// Add the namespace of the given schema to the namespace lookup table
        /// </summary>
        public void AddSchema(Schema schema)
        {
            Debug.Assert(schema.DataModel == _dataModel, "DataModel must match");
 
            if (_namespaceLookUpTable.Count == 0 && schema.DataModel != SchemaDataModelOption.ProviderManifestModel)
            {
                // Add the primitive type namespace to the namespace look up table
                if (this.PrimitiveSchema.Namespace != null)
                {
                    _namespaceLookUpTable.Add(this.PrimitiveSchema.Namespace);
                }
            }
                                    
            // Add the namespace to the namespaceLookUpTable. 
            // Its okay to have multiple schemas with the same namespace
            _namespaceLookUpTable.Add(schema.Namespace);
        }
 
        /// <summary>
        /// Resolve the type - if the type is not found, return appropriate error
        /// </summary>
        /// <returns></returns>
        public bool TryResolveType(string namespaceName, string typeName, out SchemaType schemaType)
        {
            // For resolving entity container names, namespace can be null
            string fullyQualifiedName = String.IsNullOrEmpty(namespaceName) ? typeName : namespaceName + "." + typeName;
 
            schemaType = SchemaTypes.LookUpEquivalentKey(fullyQualifiedName);
            if (schemaType != null)
            {
                return true;
            }
 
            return false;
        }
 
        /// <summary>
        /// Returns true if this is a valid namespace name or else returns false
        /// </summary>
        public bool IsValidNamespaceName(string namespaceName)
        {
            return _namespaceLookUpTable.Contains(namespaceName);
        }
        #endregion // Public Methods
 
        #region Private Methods
 
        /// <summary>
        /// Checks if the xml reader has base uri. If it doesn't have, it adds error, other
        /// returns the location from the base uri
        /// </summary>
        /// <returns></returns>
        internal static bool TryGetBaseUri(XmlReader xmlReader, out string location)
        {
            string baseUri = xmlReader.BaseURI;
            Uri uri = null;
 
            if (!string.IsNullOrEmpty(baseUri) &&
                 Uri.TryCreate(baseUri, UriKind.Absolute, out uri) &&
                 uri.Scheme == "file")
            {
                location = Helper.GetFileNameFromUri(uri);
                return true;
            }
            else
            {
                location = null;                
                return false;
            }
        }
 
        /// <summary>
        /// Add the given list of newErrors to the error collection. If there is a error in the new errors,
        /// it sets the errorEncountered to true. Returns true if the number of errors encountered is more 
        /// than max errors
        /// </summary>
        /// <returns></returns>
        private static bool UpdateErrorCollectionAndCheckForMaxErrors(List<EdmSchemaError> errorCollection, 
            IList<EdmSchemaError> newErrors, ref bool errorEncountered)
        {
            // If we have encountered error already in one of the schemas, then we don't need to check for errors in the remaining schemas.
            // Just keep aggregating the errors and throw them at the end.
            if (!errorEncountered)
            {
                if (!MetadataHelper.CheckIfAllErrorsAreWarnings(newErrors))
                {
                    errorEncountered = true;
                }
            }
 
            // Add the new errors to the error collection
            errorCollection.AddRange(newErrors);
 
            if (errorEncountered && 
                errorCollection.Where(e => e.Severity == EdmSchemaErrorSeverity.Error).Count() > MaxErrorCount)
            {
                return true;
            }
            return false;
        }
        #endregion
 
        #region Internal Properties
 
        internal SchemaElementLookUpTable<SchemaType> SchemaTypes
        {
            get
            {
                return _schemaTypes;
            }
        }
 
        internal DbProviderManifest GetProviderManifest(Action<string, ErrorCode, EdmSchemaErrorSeverity> addError)
        { 
            if (_providerManifest == null)
            {
                _providerManifest = _providerManifestNeeded(addError);
            }
            return _providerManifest;
        }
 
        internal SchemaDataModelOption DataModel { get { return _dataModel; } }
 
        internal void EnsurePrimitiveSchemaIsLoaded(double forSchemaVersion)
        {
            if (_primitiveSchema == null)
            {
                this.effectiveSchemaVersion = forSchemaVersion;
                _primitiveSchema = new PrimitiveSchema(this);
            }
        }
 
        internal PrimitiveSchema PrimitiveSchema
        {
            get
            {
                return _primitiveSchema;
            }
        }
 
        internal AttributeValueNotification ProviderNotification
        {
            get
            {
                return _providerNotification;
            }
        }
 
        internal AttributeValueNotification ProviderManifestTokenNotification
        {
            get
            {
                return _providerManifestTokenNotification;
            }
        }
 
        #endregion
    }
}