File: System\Data\Entity\Design\Common\OneToOneMappingSerializer.cs
Project: ndp\fx\src\DataEntityDesign\Design\System.Data.Entity.Design.csproj (System.Data.Entity.Design)
//---------------------------------------------------------------------
// <copyright file="OneToOneMappingSerializer.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
//
// @owner       Microsoft
// @backupOwner Microsoft
//---------------------------------------------------------------------
using System.Collections.Generic;
using System.Data.Common;
using System.Data.Common.Utils;
using System.Data.Mapping;
using System.Data.Metadata.Edm;
using System.Diagnostics;
using System.Linq;
using System.Xml;
 
namespace System.Data.Entity.Design.Common
{
    internal class OneToOneMappingSerializer
    {
        internal class MappingLookups
        {
            internal Dictionary<EntityType, EntityType> StoreEntityTypeToModelEntityType = new Dictionary<EntityType, EntityType>();
            internal Dictionary<EdmProperty, EdmProperty> StoreEdmPropertyToModelEdmProperty = new Dictionary<EdmProperty, EdmProperty>();
            internal Dictionary<EntitySet, EntitySet> StoreEntitySetToModelEntitySet = new Dictionary<EntitySet, EntitySet>();
            
            internal Dictionary<AssociationType, AssociationType> StoreAssociationTypeToModelAssociationType = new Dictionary<AssociationType, AssociationType>();
            internal Dictionary<AssociationEndMember, AssociationEndMember> StoreAssociationEndMemberToModelAssociationEndMember = new Dictionary<AssociationEndMember, AssociationEndMember>();
            internal Dictionary<AssociationSet, AssociationSet> StoreAssociationSetToModelAssociationSet = new Dictionary<AssociationSet, AssociationSet>();
            internal Dictionary<AssociationSetEnd, AssociationSetEnd> StoreAssociationSetEndToModelAssociationSetEnd = new Dictionary<AssociationSetEnd, AssociationSetEnd>();
 
            internal List<CollapsedEntityAssociationSet> CollapsedEntityAssociationSets = new List<CollapsedEntityAssociationSet>();
 
            internal List<Tuple<EdmFunction, EdmFunction>> StoreFunctionToFunctionImport = new List<Tuple<EdmFunction, EdmFunction>>();
        }
 
        // this class represents a construct found in the ssdl where a link table
        // contained no data (all its properties were part of its keys)
        // it has exactly two associations
        // the entity type is the TO side of both associations          
        // all the colums are used as TO columns in the constraint
        internal class CollapsedEntityAssociationSet
        {
            private EntitySet _storeEntitySet;
            private List<AssociationSet> _storeAssociationSets = new List<AssociationSet>(2);
            private AssociationSet _modelAssociationSet;
 
            public AssociationSet ModelAssociationSet
            {
                get { return _modelAssociationSet; }
                set 
                {
                    Debug.Assert(_modelAssociationSet == null, "why is this getting set multiple times, it should only be set after the new set is created");
                    _modelAssociationSet = value;
                }
            }
 
            public CollapsedEntityAssociationSet(EntitySet entitySet)
            {
                Debug.Assert(entitySet != null, "entitySet parameter is null");
                _storeEntitySet = entitySet;
            }
 
            public EntitySet EntitySet
            {
                get { return _storeEntitySet; }
            }
 
            public List<AssociationSet> AssociationSets
            {
                get { return _storeAssociationSets; }
            }
            
            public void GetStoreAssociationSetEnd(int index, out AssociationSetEnd storeAssociationSetEnd, out RelationshipMultiplicity multiplicity, out OperationAction deleteBehavior)
            {
                Debug.Assert(index >= 0 && index < AssociationSets.Count, "out of bounds dude!!");
                Debug.Assert(AssociationSets.Count == 2, "This code depends on only having exactly two AssociationSets");
                GetFromAssociationSetEnd(AssociationSets[index], AssociationSets[(index+1)%2], out storeAssociationSetEnd, out multiplicity, out deleteBehavior);
            }
 
            private void GetFromAssociationSetEnd(AssociationSet definingSet, AssociationSet multiplicitySet, out AssociationSetEnd associationSetEnd, out RelationshipMultiplicity multiplicity, out OperationAction deleteBehavior)
            {
                // for a situation like this (CD is CascadeDelete)
                // 
                // --------  CD   --------  CD   --------
                // | A    |1 <-  1| AtoB |* <-  1|  B   |  
                // |      |-------|      |-------|      | 
                // |      |       |      |       |      |
                // --------       --------       --------
                // 
                // You get
                // --------  CD   --------
                // |  A   |* <-  1|  B   |
                // |      |-------|      |
                // |      |       |      |
                // --------       --------
                // 
                // Notice that the of the new "link table association" muliplicities are opposite of what comming into the original link table
                // this seems counter intuitive at first, but makes sense when you think all the way through it
                //
                // CascadeDelete Behavior (we can assume the runtime will always delete cascade 
                //                         to the link table from the outside tables (it actually doesn't, but that is a bug))
                //  Store               Effective
                //  A -> AToB <- B      None
                //  A <- AToB <- B      <-
                //  A -> AToB -> B      ->
                //  A <- AToB -> B      None
                //  A <- AToB    B      <-
                //  A    AToB -> B      ->
                //  A -> AToB    B      None
                //  A    AToB <- B      None
                //  
                //  Other CascadeDelete rules
                //  1. Can't have a delete from a Many multiplicity end
                //  2. Can't have a delete on both ends
                //
 
                associationSetEnd = GetAssociationSetEnd(definingSet, true);
                AssociationSetEnd multiplicityAssociationSetEnd = GetAssociationSetEnd(multiplicitySet, false);
                multiplicity = multiplicityAssociationSetEnd.CorrespondingAssociationEndMember.RelationshipMultiplicity;
                deleteBehavior = OperationAction.None;
                if (multiplicity != RelationshipMultiplicity.Many)
                {
                    OperationAction otherEndBehavior = GetAssociationSetEnd(definingSet, false).CorrespondingAssociationEndMember.DeleteBehavior;
                    if(otherEndBehavior == OperationAction.None)
                    {
                        // Since the other end does not have an operation
                        // that means that only one end could possibly have an operation, that is good
                        // so set it the operation
                        deleteBehavior = multiplicityAssociationSetEnd.CorrespondingAssociationEndMember.DeleteBehavior;
                    }
                }
            }
 
            private static AssociationSetEnd GetAssociationSetEnd(AssociationSet set, bool fromEnd)
            {
                Debug.Assert(set.ElementType.ReferentialConstraints.Count == 1, "no referenctial constraint for association[0]");
                ReferentialConstraint constraint = set.ElementType.ReferentialConstraints[0];
 
                Debug.Assert(set.AssociationSetEnds.Count == 2, "Associations are assumed to have two ends");
                int toEndIndex, fromEndIndex;
                if (set.AssociationSetEnds[0].CorrespondingAssociationEndMember == constraint.FromRole)
                {
                    fromEndIndex = 0;
                    toEndIndex = 1;
                   
                }
                else
                {
                    fromEndIndex = 1;
                    toEndIndex = 0;
                }
 
 
                if (fromEnd)
                {
                    return set.AssociationSetEnds[fromEndIndex];
                }
                else
                {
                    return set.AssociationSetEnds[toEndIndex];
                }
            }
 
            public bool MeetsRequirementsForCollapsableAssociation
            {
                get
                {
                    if (_storeAssociationSets.Count != 2)
                        return false;
 
                    ReferentialConstraint constraint0;
                    ReferentialConstraint constraint1;
                    GetConstraints(out constraint0, out constraint1);
                    if (!IsEntityDependentSideOfBothAssociations(constraint0, constraint1))
                        return false;
 
                    if (!IsAtLeastOneColumnOfBothDependentRelationshipColumnSetsNonNullable(constraint0, constraint1))
                        return false;
 
                    if (!AreAllEntityColumnsMappedAsToColumns(constraint0, constraint1))
                        return false;
 
                    if (IsAtLeastOneColumnFKInBothAssociations(constraint0, constraint1))
                        return false;
 
                    return true;
                }
            }
 
            private bool IsAtLeastOneColumnFKInBothAssociations(ReferentialConstraint constraint0, ReferentialConstraint constraint1)
            {
                return constraint1.ToProperties.Any(c => constraint0.ToProperties.Contains(c));
            }
 
            private bool IsAtLeastOneColumnOfBothDependentRelationshipColumnSetsNonNullable(ReferentialConstraint constraint0, ReferentialConstraint constraint1)
            {
                return ToPropertyHasNonNullableColumn(constraint0) && ToPropertyHasNonNullableColumn(constraint1);
            }
 
            private static bool ToPropertyHasNonNullableColumn(ReferentialConstraint constraint)
            {
                foreach (EdmProperty property in constraint.ToProperties)
                {
                    if (!property.Nullable)
                    {
                        return true;
                    }
                }
                return false;
            }
 
            private bool AreAllEntityColumnsMappedAsToColumns(ReferentialConstraint constraint0, ReferentialConstraint constraint1)
            {
                Set<string> names = new Set<string>();
                AddToPropertyNames(constraint0, names);
                AddToPropertyNames(constraint1, names);
                return names.Count == _storeEntitySet.ElementType.Properties.Count;
            }
 
            private static void AddToPropertyNames(ReferentialConstraint constraint, Set<string> names)
            {
                foreach (EdmProperty property in constraint.ToProperties)
                {
                    names.Add(property.Name);
                }
            }
 
            private bool IsEntityDependentSideOfBothAssociations(ReferentialConstraint constraint0, ReferentialConstraint constraint1)
            {
                return ((RefType)constraint0.ToRole.TypeUsage.EdmType).ElementType == _storeEntitySet.ElementType && ((RefType)constraint1.ToRole.TypeUsage.EdmType).ElementType == _storeEntitySet.ElementType;
            }
 
            private void GetConstraints(out ReferentialConstraint constraint0, out ReferentialConstraint constraint1)
            {
                Debug.Assert(_storeAssociationSets.Count == 2, "don't call this method if you don't have two associations");
                Debug.Assert(_storeAssociationSets[0].ElementType.ReferentialConstraints.Count == 1, "no referenctial constraint for association[0]");
                Debug.Assert(_storeAssociationSets[1].ElementType.ReferentialConstraints.Count == 1, "no referenctial constraint for association[1]");
                constraint0 = _storeAssociationSets[0].ElementType.ReferentialConstraints[0];
                constraint1 = _storeAssociationSets[1].ElementType.ReferentialConstraints[0];
            }
        }
 
        private MappingLookups _lookups;
        private EntityContainer _storeContainer;
        private EntityContainer _modelContainer;
        private string _xmlNamespace;
 
        internal OneToOneMappingSerializer(MappingLookups lookups,
            EntityContainer storeContainer,
            EntityContainer modelContainer,
            Version schemaVersion)
        {
            EDesignUtil.CheckArgumentNull(lookups, "lookups");
            EDesignUtil.CheckArgumentNull(storeContainer, "storeContainer");
            EDesignUtil.CheckArgumentNull(modelContainer, "modelContainer");
            _lookups = lookups;
            _storeContainer = storeContainer;
            _modelContainer = modelContainer;
            _xmlNamespace = EntityFrameworkVersions.GetSchemaNamespace(schemaVersion, DataSpace.CSSpace);
        }
       
        public void WriteXml(XmlWriter writer)
        {
            EDesignUtil.CheckArgumentNull(writer, "writer");
 
            WriteMappingStartElement(writer);
            WriteEntityContainerMappingElement(writer);
            writer.WriteEndElement();
        }
 
        private void WriteEntityContainerMappingElement(XmlWriter writer)
        {
            writer.WriteStartElement(StorageMslConstructs.EntityContainerMappingElement, _xmlNamespace);
            writer.WriteAttributeString(StorageMslConstructs.StorageEntityContainerAttribute, _storeContainer.Name);
            writer.WriteAttributeString(StorageMslConstructs.CdmEntityContainerAttribute, _modelContainer.Name);
 
            foreach (EntitySet set in _lookups.StoreEntitySetToModelEntitySet.Keys)
            {
                EntitySet modelEntitySet = _lookups.StoreEntitySetToModelEntitySet[set];
                WriteEntitySetMappingElement(writer, set, modelEntitySet);
            }
 
            foreach(AssociationSet set in _lookups.StoreAssociationSetToModelAssociationSet.Keys)
            {
                AssociationSet modelAssociationSet = _lookups.StoreAssociationSetToModelAssociationSet[set];
                WriteAssociationSetMappingElement(writer, set, modelAssociationSet);
            }
 
            foreach (CollapsedEntityAssociationSet set in _lookups.CollapsedEntityAssociationSets)
            {
                WriteAssociationSetMappingElement(writer, set);
            }
 
            foreach (var functionMapping in _lookups.StoreFunctionToFunctionImport)
            {
                var storeFunction = functionMapping.Item1;
                var functionImport = functionMapping.Item2;
                WriteFunctionImportMappingElement(writer, storeFunction, functionImport);
            }
 
            writer.WriteEndElement();
        }
 
        private void WriteFunctionImportMappingElement(XmlWriter writer, EdmFunction storeFunction, EdmFunction functionImport)
        {
            Debug.Assert(storeFunction.IsComposableAttribute, "storeFunction.IsComposableAttribute");
            Debug.Assert(storeFunction.ReturnParameters.Count == 1, "storeFunction.ReturnParameters.Count == 1");
            Debug.Assert(functionImport.IsComposableAttribute, "functionImport.IsComposableAttribute");
            Debug.Assert(functionImport.ReturnParameters.Count == 1, "functionImport.ReturnParameters.Count == 1");
 
            writer.WriteStartElement(StorageMslConstructs.FunctionImportMappingElement, _xmlNamespace);
            writer.WriteAttributeString(StorageMslConstructs.FunctionImportMappingFunctionNameAttribute, storeFunction.FullName);
            writer.WriteAttributeString(StorageMslConstructs.FunctionImportMappingFunctionImportNameAttribute, functionImport.Name);
 
            RowType tvfReturnType = TypeHelpers.GetTvfReturnType(storeFunction);
            if (tvfReturnType != null)
            {
                // Table-valued function
                Debug.Assert(functionImport.ReturnParameter.TypeUsage.EdmType.BuiltInTypeKind == BuiltInTypeKind.CollectionType, "functionImport is expected to return Collection(ComplexType)");
                var modelCollectionType = (CollectionType)functionImport.ReturnParameter.TypeUsage.EdmType;
                Debug.Assert(modelCollectionType.TypeUsage.EdmType.BuiltInTypeKind == BuiltInTypeKind.ComplexType, "functionImport is expected to return Collection(ComplexType)");
                var modelComplexType = (ComplexType)modelCollectionType.TypeUsage.EdmType;
 
                // Write ResultMapping/ComplexTypeMapping
                writer.WriteStartElement(StorageMslConstructs.FunctionImportMappingResultMapping, _xmlNamespace);
                writer.WriteStartElement(StorageMslConstructs.ComplexTypeMappingElement, _xmlNamespace);
                writer.WriteAttributeString(StorageMslConstructs.ComplexTypeMappingTypeNameAttribute, modelComplexType.FullName);
                foreach (EdmProperty storeProperty in tvfReturnType.Properties)
                {
                    EdmProperty modelProperty = _lookups.StoreEdmPropertyToModelEdmProperty[storeProperty];
                    WriteScalarPropertyElement(writer, storeProperty, modelProperty);
                }
                writer.WriteEndElement();
                writer.WriteEndElement();
            }
            else
            {
                Debug.Fail("Only TVF store functions are supported.");
            }
 
            writer.WriteEndElement();
        }
 
        private void WriteAssociationSetMappingElement(XmlWriter writer, CollapsedEntityAssociationSet collapsedAssociationSet)
        {
            if (!collapsedAssociationSet.ModelAssociationSet.ElementType.IsForeignKey)
            {
                writer.WriteStartElement(StorageMslConstructs.AssociationSetMappingElement, _xmlNamespace);
                writer.WriteAttributeString(StorageMslConstructs.AssociationSetMappingNameAttribute, collapsedAssociationSet.ModelAssociationSet.Name);
                writer.WriteAttributeString(StorageMslConstructs.AssociationSetMappingTypeNameAttribute, collapsedAssociationSet.ModelAssociationSet.ElementType.FullName);
                writer.WriteAttributeString(StorageMslConstructs.AssociationSetMappingStoreEntitySetAttribute, collapsedAssociationSet.EntitySet.Name);
 
 
                for (int i = 0; i < collapsedAssociationSet.AssociationSets.Count; i++)
                {
                    AssociationSetEnd storeEnd;
                    RelationshipMultiplicity multiplicity;
                    OperationAction deleteBehavior;
                    collapsedAssociationSet.GetStoreAssociationSetEnd(i, out storeEnd, out multiplicity, out deleteBehavior);
                    AssociationSetEnd modelEnd = _lookups.StoreAssociationSetEndToModelAssociationSetEnd[storeEnd];
                    WriteEndPropertyElement(writer, storeEnd, modelEnd);
                }
 
                // don't need condition element
 
                writer.WriteEndElement();
            }
        }
 
        private void WriteAssociationSetMappingElement(XmlWriter writer, AssociationSet store, AssociationSet model)
        {
            if (!model.ElementType.IsForeignKey)
            {
                writer.WriteStartElement(StorageMslConstructs.AssociationSetMappingElement, _xmlNamespace);
                writer.WriteAttributeString(StorageMslConstructs.AssociationSetMappingNameAttribute, model.Name);
                writer.WriteAttributeString(StorageMslConstructs.AssociationSetMappingTypeNameAttribute, model.ElementType.FullName);
 
                // all column names must be the primary key of the 
                // end, but as columns in the Fk table.
                AssociationSetEnd foreignKeyTableEnd = GetAssociationSetEndForForeignKeyTable(store);
                writer.WriteAttributeString(StorageMslConstructs.AssociationSetMappingStoreEntitySetAttribute, foreignKeyTableEnd.EntitySet.Name);
 
                foreach (AssociationSetEnd storeEnd in store.AssociationSetEnds)
                {
                    AssociationSetEnd modelEnd = _lookups.StoreAssociationSetEndToModelAssociationSetEnd[storeEnd];
                    WriteEndPropertyElement(writer, storeEnd, modelEnd);
                }
 
                ReferentialConstraint constraint = GetReferentialConstraint(store);
                foreach (EdmProperty fkColumn in constraint.ToProperties)
                {
                    if (fkColumn.Nullable)
                    {
                        WriteConditionElement(writer, fkColumn);
                    }
                }
 
                writer.WriteEndElement();
            }
        }
 
        private void WriteConditionElement(XmlWriter writer, EdmProperty fkColumn)
        {
            writer.WriteStartElement(StorageMslConstructs.ConditionElement, _xmlNamespace);
            writer.WriteAttributeString(StorageMslConstructs.ConditionColumnNameAttribute, fkColumn.Name);
            writer.WriteAttributeString(StorageMslConstructs.ConditionIsNullAttribute, "false");
            writer.WriteEndElement();
        }
 
        private static AssociationSetEnd GetAssociationSetEndForForeignKeyTable(AssociationSet store)
        {
            ReferentialConstraint constraint = GetReferentialConstraint(store);
            return store.AssociationSetEnds.GetValue(constraint.ToRole.Name, false);
        }
 
        internal static ReferentialConstraint GetReferentialConstraint(AssociationSet set)
        {
            // this seeems like a hack, but it is what we have right now.
            ReferentialConstraint constraint = null;
            foreach (ReferentialConstraint rc in set.ElementType.ReferentialConstraints)
            {
                Debug.Assert(constraint == null, "we should only get one");
                constraint = rc;
            }
            Debug.Assert(constraint != null, "we should get at least one constraint");
            return constraint;
        }
 
        private void WriteEndPropertyElement(XmlWriter writer, AssociationSetEnd store, AssociationSetEnd model)
        {
            writer.WriteStartElement(StorageMslConstructs.EndPropertyMappingElement, _xmlNamespace);
            writer.WriteAttributeString(StorageMslConstructs.EndPropertyMappingNameAttribute, model.Name);
            foreach (EdmProperty storeKeyMember in store.EntitySet.ElementType.KeyMembers)
            {
                EdmProperty modelKeyMember = _lookups.StoreEdmPropertyToModelEdmProperty[storeKeyMember];
                EdmProperty storeFkTableMember = GetAssociatedFkColumn(store, storeKeyMember);
                WriteScalarPropertyElement(writer, storeFkTableMember, modelKeyMember);
            }
            writer.WriteEndElement();
        }
 
        private static EdmProperty GetAssociatedFkColumn(AssociationSetEnd store, EdmProperty storeKeyProperty)
        {
            ReferentialConstraint constraint = GetReferentialConstraint(store.ParentAssociationSet);
            if (store.Name == constraint.FromRole.Name)
            {
                for (int i = 0; i < constraint.FromProperties.Count; i++)
                {
                    if (constraint.FromProperties[i] == storeKeyProperty)
                    {
                        // return the matching Fk column
                        return constraint.ToProperties[i];
                    }
                }
            }
 
                return storeKeyProperty;
        }
 
        private void WriteEntitySetMappingElement(XmlWriter writer, EntitySet store, EntitySet model)
        {
            writer.WriteStartElement(StorageMslConstructs.EntitySetMappingElement, _xmlNamespace);
            writer.WriteAttributeString(StorageMslConstructs.EntitySetMappingNameAttribute, model.Name);
            WriteEntityTypeMappingElement(writer, store, model);
            writer.WriteEndElement();
        }
 
        private void WriteEntityTypeMappingElement(XmlWriter writer, EntitySet store, EntitySet model)
        {
            writer.WriteStartElement(StorageMslConstructs.EntityTypeMappingElement, _xmlNamespace);
            writer.WriteAttributeString(StorageMslConstructs.EntityTypeMappingTypeNameAttribute, model.ElementType.FullName);
            WriteMappingFragmentElement(writer, store, model);
            writer.WriteEndElement();
        }
 
        private void WriteMappingFragmentElement(XmlWriter writer, EntitySet store, EntitySet model)
        {
            writer.WriteStartElement(StorageMslConstructs.MappingFragmentElement, _xmlNamespace);
            writer.WriteAttributeString(StorageMslConstructs.EntityTypeMappingStoreEntitySetAttribute, store.Name);
            foreach (EdmProperty storeProperty in store.ElementType.Properties)
            {
                // we don't add the fk properties to c-space, so some are missing,
                // check to see if we have a map for this one
                if (_lookups.StoreEdmPropertyToModelEdmProperty.ContainsKey(storeProperty))
                {
                    EdmProperty modelProperty = _lookups.StoreEdmPropertyToModelEdmProperty[storeProperty];
                    WriteScalarPropertyElement(writer, storeProperty, modelProperty);
                }
            }
            writer.WriteEndElement();
        }
 
        private void WriteScalarPropertyElement(XmlWriter writer, EdmProperty store, EdmProperty model)
        {
            Debug.Assert(store.TypeUsage.EdmType.BuiltInTypeKind == BuiltInTypeKind.PrimitiveType, "only expect scalar type properties");
            Debug.Assert(model.TypeUsage.EdmType.BuiltInTypeKind == BuiltInTypeKind.PrimitiveType, "only expect scalar type properties");
 
            writer.WriteStartElement(StorageMslConstructs.ScalarPropertyElement, _xmlNamespace);
            writer.WriteAttributeString(StorageMslConstructs.ScalarPropertyNameAttribute, model.Name);
            writer.WriteAttributeString(StorageMslConstructs.ScalarPropertyColumnNameAttribute, store.Name);
            writer.WriteEndElement();
        }
 
        private void WriteMappingStartElement(XmlWriter writer)
        {
            writer.WriteStartElement(StorageMslConstructs.MappingElement, _xmlNamespace);
            writer.WriteAttributeString(StorageMslConstructs.MappingSpaceAttribute, "C-S");
        }
    }
}