//--------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // // // @owner Microsoft // @backupOwner willa //--------------------------------------------------------------------- namespace System.Data.Mapping { using System.Collections; using System.Collections.Generic; using System.Data.Common.Utils; using System.Data.Common.Utils.Boolean; using System.Data.Entity; using System.Data.Metadata.Edm; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Xml; using System.Xml.XPath; using OM = System.Collections.ObjectModel; /// /// Represents a mapping from a model function import to a store composable or non-composable function. /// internal abstract class FunctionImportMapping { internal FunctionImportMapping(EdmFunction functionImport, EdmFunction targetFunction) { this.FunctionImport = EntityUtil.CheckArgumentNull(functionImport, "functionImport"); this.TargetFunction = EntityUtil.CheckArgumentNull(targetFunction, "targetFunction"); } /// /// Gets model function (or source of the mapping) /// internal readonly EdmFunction FunctionImport; /// /// Gets store function (or target of the mapping) /// internal readonly EdmFunction TargetFunction; } internal sealed class FunctionImportStructuralTypeMappingKB { internal FunctionImportStructuralTypeMappingKB( IEnumerable structuralTypeMappings, ItemCollection itemCollection) { EntityUtil.CheckArgumentNull(structuralTypeMappings, "structuralTypeMappings"); m_itemCollection = EntityUtil.CheckArgumentNull(itemCollection, "itemCollection"); // If no specific type mapping. if (structuralTypeMappings.Count() == 0) { // Initialize with defaults. this.ReturnTypeColumnsRenameMapping = new Dictionary(); this.NormalizedEntityTypeMappings = new OM.ReadOnlyCollection(new List()); this.DiscriminatorColumns = new OM.ReadOnlyCollection(new List()); this.MappedEntityTypes = new OM.ReadOnlyCollection(new List()); return; } IEnumerable entityTypeMappings = structuralTypeMappings.OfType(); // FunctionImportEntityTypeMapping if (null != entityTypeMappings && null != entityTypeMappings.FirstOrDefault()) { var isOfTypeEntityTypeColumnsRenameMapping = new Dictionary>(); var entityTypeColumnsRenameMapping = new Dictionary>(); var normalizedEntityTypeMappings = new List(); // Collect all mapped entity types. this.MappedEntityTypes = entityTypeMappings .SelectMany(mapping => mapping.GetMappedEntityTypes(m_itemCollection)) .Distinct() .ToList() .AsReadOnly(); // Collect all discriminator columns. this.DiscriminatorColumns = entityTypeMappings .SelectMany(mapping => mapping.GetDiscriminatorColumns()) .Distinct() .ToList() .AsReadOnly(); m_entityTypeLineInfos = new KeyToListMap(EqualityComparer.Default); m_isTypeOfLineInfos = new KeyToListMap(EqualityComparer.Default); foreach (var entityTypeMapping in entityTypeMappings) { // Remember LineInfos for error reporting. foreach (var entityType in entityTypeMapping.EntityTypes) { m_entityTypeLineInfos.Add(entityType, entityTypeMapping.LineInfo); } foreach (var isTypeOf in entityTypeMapping.IsOfTypeEntityTypes) { m_isTypeOfLineInfos.Add(isTypeOf, entityTypeMapping.LineInfo); } // Create map from column name to condition. var columnMap = entityTypeMapping.Conditions.ToDictionary( condition => condition.ColumnName, condition => condition); // Align conditions with discriminator columns. var columnMappings = new List(this.DiscriminatorColumns.Count); for (int i = 0; i < this.DiscriminatorColumns.Count; i++) { string discriminatorColumn = this.DiscriminatorColumns[i]; FunctionImportEntityTypeMappingCondition mappingCondition; if (columnMap.TryGetValue(discriminatorColumn, out mappingCondition)) { columnMappings.Add(mappingCondition); } else { // Null indicates the value for this discriminator doesn't matter. columnMappings.Add(null); } } // Create bit map for implied entity types. bool[] impliedEntityTypesBitMap = new bool[this.MappedEntityTypes.Count]; var impliedEntityTypesSet = new Set(entityTypeMapping.GetMappedEntityTypes(m_itemCollection)); for (int i = 0; i < this.MappedEntityTypes.Count; i++) { impliedEntityTypesBitMap[i] = impliedEntityTypesSet.Contains(this.MappedEntityTypes[i]); } // Construct normalized mapping. normalizedEntityTypeMappings.Add(new FunctionImportNormalizedEntityTypeMapping(this, columnMappings, new BitArray(impliedEntityTypesBitMap))); // Construct the rename mappings by adding isTypeOf types and specific entity types to the corresponding lists. foreach (var isOfType in entityTypeMapping.IsOfTypeEntityTypes) { if (!isOfTypeEntityTypeColumnsRenameMapping.Keys.Contains(isOfType)) { isOfTypeEntityTypeColumnsRenameMapping.Add(isOfType, new OM.Collection()); } foreach (var rename in entityTypeMapping.ColumnsRenameList) { isOfTypeEntityTypeColumnsRenameMapping[isOfType].Add(rename); } } foreach (var entityType in entityTypeMapping.EntityTypes) { if (!entityTypeColumnsRenameMapping.Keys.Contains(entityType)) { entityTypeColumnsRenameMapping.Add(entityType, new OM.Collection()); } foreach (var rename in entityTypeMapping.ColumnsRenameList) { entityTypeColumnsRenameMapping[entityType].Add(rename); } } } this.ReturnTypeColumnsRenameMapping = new FunctionImportReturnTypeEntityTypeColumnsRenameBuilder(isOfTypeEntityTypeColumnsRenameMapping, entityTypeColumnsRenameMapping) .ColumnRenameMapping; this.NormalizedEntityTypeMappings = new OM.ReadOnlyCollection( normalizedEntityTypeMappings); } else { // FunctionImportComplexTypeMapping Debug.Assert(structuralTypeMappings.First() is FunctionImportComplexTypeMapping, "only two types can have renames, complexType and entityType"); IEnumerable complexTypeMappings = structuralTypeMappings.Cast(); Debug.Assert(complexTypeMappings.Count() == 1, "how come there are more than 1, complex type cannot derive from other complex type"); this.ReturnTypeColumnsRenameMapping = new Dictionary(); foreach (var rename in complexTypeMappings.First().ColumnsRenameList) { FunctionImportReturnTypeStructuralTypeColumnRenameMapping columnRenameMapping = new FunctionImportReturnTypeStructuralTypeColumnRenameMapping(rename.CMember); columnRenameMapping.AddRename(new FunctionImportReturnTypeStructuralTypeColumn( rename.SColumn, complexTypeMappings.First().ReturnType, false, rename.LineInfo)); this.ReturnTypeColumnsRenameMapping.Add(rename.CMember, columnRenameMapping); } // Initialize the entity mapping data as empty. this.NormalizedEntityTypeMappings = new OM.ReadOnlyCollection(new List()); this.DiscriminatorColumns = new OM.ReadOnlyCollection(new List() { }); this.MappedEntityTypes = new OM.ReadOnlyCollection(new List() { }); } } private readonly ItemCollection m_itemCollection; private readonly KeyToListMap m_entityTypeLineInfos; private readonly KeyToListMap m_isTypeOfLineInfos; /// /// Gets all types in scope for this mapping. /// internal readonly OM.ReadOnlyCollection MappedEntityTypes; /// /// Gets a list of all discriminator columns used in this mapping. /// internal readonly OM.ReadOnlyCollection DiscriminatorColumns; /// /// Gets normalized representation of all EntityTypeMapping fragments for this /// function import mapping. /// internal readonly OM.ReadOnlyCollection NormalizedEntityTypeMappings; /// /// Get the columns rename mapping for return type, the first string is the member name /// the second one is column names for different types that mentioned in the mapping. /// internal readonly Dictionary ReturnTypeColumnsRenameMapping; internal bool ValidateTypeConditions(bool validateAmbiguity, IList errors, string sourceLocation) { // Verify that all types can be produced KeyToListMap unreachableEntityTypes; KeyToListMap unreachableIsTypeOfs; GetUnreachableTypes(validateAmbiguity, out unreachableEntityTypes, out unreachableIsTypeOfs); bool valid = true; foreach (var unreachableEntityType in unreachableEntityTypes.KeyValuePairs) { var lineInfo = unreachableEntityType.Value.First(); string lines = StringUtil.ToCommaSeparatedString(unreachableEntityType.Value.Select(li => li.LineNumber)); EdmSchemaError error = new EdmSchemaError( Strings.Mapping_FunctionImport_UnreachableType(unreachableEntityType.Key.FullName, lines), (int)StorageMappingErrorCode.MappingFunctionImportAmbiguousTypeConditions, EdmSchemaErrorSeverity.Error, sourceLocation, lineInfo.LineNumber, lineInfo.LinePosition); errors.Add(error); valid = false; } foreach (var unreachableIsTypeOf in unreachableIsTypeOfs.KeyValuePairs) { var lineInfo = unreachableIsTypeOf.Value.First(); string lines = StringUtil.ToCommaSeparatedString(unreachableIsTypeOf.Value.Select(li => li.LineNumber)); string isTypeOfDescription = StorageMslConstructs.IsTypeOf + unreachableIsTypeOf.Key.FullName + StorageMslConstructs.IsTypeOfTerminal; EdmSchemaError error = new EdmSchemaError( Strings.Mapping_FunctionImport_UnreachableIsTypeOf(isTypeOfDescription, lines), (int)StorageMappingErrorCode.MappingFunctionImportAmbiguousTypeConditions, EdmSchemaErrorSeverity.Error, sourceLocation, lineInfo.LineNumber, lineInfo.LinePosition); errors.Add(error); valid = false; } return valid; } /// /// Determines which explicitly mapped types in the function import mapping cannot be generated. /// For IsTypeOf declarations, reports if no type in hierarchy can be produced. /// /// Works by: /// /// - Converting type mapping conditions into vertices /// - Checking that some assignment satisfies /// private void GetUnreachableTypes( bool validateAmbiguity, out KeyToListMap unreachableEntityTypes, out KeyToListMap unreachableIsTypeOfs) { // Contains, for each DiscriminatorColumn, a domain variable where the domain values are // integers representing the ordinal within discriminatorDomains. DomainVariable[] variables = ConstructDomainVariables(); // Convert type mapping conditions to decision diagram vertices. var converter = new DomainConstraintConversionContext(); Vertex[] mappingConditions = ConvertMappingConditionsToVertices(converter, variables); // Find reachable types. Set reachableTypes = validateAmbiguity ? FindUnambiguouslyReachableTypes(converter, mappingConditions) : FindReachableTypes(converter, mappingConditions); CollectUnreachableTypes(reachableTypes, out unreachableEntityTypes, out unreachableIsTypeOfs); } private DomainVariable[] ConstructDomainVariables() { // Determine domain for each discriminator column, including "other" and "null" placeholders. var discriminatorDomains = new Set[this.DiscriminatorColumns.Count]; for (int i = 0; i < discriminatorDomains.Length; i++) { discriminatorDomains[i] = new Set(); discriminatorDomains[i].Add(ValueCondition.IsOther); discriminatorDomains[i].Add(ValueCondition.IsNull); } // Collect all domain values. foreach (var typeMapping in this.NormalizedEntityTypeMappings) { for (int i = 0; i < this.DiscriminatorColumns.Count; i++) { var discriminatorValue = typeMapping.ColumnConditions[i]; if (null != discriminatorValue && !discriminatorValue.ConditionValue.IsNotNullCondition) // NotNull is a special range (everything but IsNull) { discriminatorDomains[i].Add(discriminatorValue.ConditionValue); } } } var discriminatorVariables = new DomainVariable[discriminatorDomains.Length]; for (int i = 0; i < discriminatorVariables.Length; i++) { // domain variable is identified by the column name and takes all collected domain values discriminatorVariables[i] = new DomainVariable( this.DiscriminatorColumns[i], discriminatorDomains[i].MakeReadOnly()); } return discriminatorVariables; } private Vertex[] ConvertMappingConditionsToVertices( ConversionContext> converter, DomainVariable[] variables) { Vertex[] conditions = new Vertex[this.NormalizedEntityTypeMappings.Count]; for (int i = 0; i < conditions.Length; i++) { var typeMapping = this.NormalizedEntityTypeMappings[i]; // create conjunction representing the condition Vertex condition = Vertex.One; for (int j = 0; j < this.DiscriminatorColumns.Count; j++) { var columnCondition = typeMapping.ColumnConditions[j]; if (null != columnCondition) { var conditionValue = columnCondition.ConditionValue; if (conditionValue.IsNotNullCondition) { // the 'not null' condition is not actually part of the domain (since it // covers other elements), so create a Not(value in {null}) condition var isNull = new TermExpr>( new DomainConstraint(variables[j], ValueCondition.IsNull)); Vertex isNullVertex = converter.TranslateTermToVertex(isNull); condition = converter.Solver.And(condition, converter.Solver.Not(isNullVertex)); } else { var hasValue = new TermExpr>( new DomainConstraint(variables[j], conditionValue)); condition = converter.Solver.And(condition, converter.TranslateTermToVertex(hasValue)); } } } conditions[i] = condition; } return conditions; } /// /// Determines which types are produced by this mapping. /// private Set FindReachableTypes(DomainConstraintConversionContext converter, Vertex[] mappingConditions) { // For each entity type, create a candidate function that evaluates to true given // discriminator assignments iff. all of that type's conditions evaluate to true // and its negative conditions evaluate to false. Vertex[] candidateFunctions = new Vertex[this.MappedEntityTypes.Count]; for (int i = 0; i < candidateFunctions.Length; i++) { // Seed the candidate function conjunction with 'true'. Vertex candidateFunction = Vertex.One; for (int j = 0; j < this.NormalizedEntityTypeMappings.Count; j++) { var entityTypeMapping = this.NormalizedEntityTypeMappings[j]; // Determine if this mapping is a positive or negative case for the current type. if (entityTypeMapping.ImpliedEntityTypes[i]) { candidateFunction = converter.Solver.And(candidateFunction, mappingConditions[j]); } else { candidateFunction = converter.Solver.And(candidateFunction, converter.Solver.Not(mappingConditions[j])); } } candidateFunctions[i] = candidateFunction; } // Make sure that for each type there is an assignment that resolves to only that type. var reachableTypes = new Set(); for (int i = 0; i < candidateFunctions.Length; i++) { // Create a function that evaluates to true iff. the current candidate function is true // and every other candidate function is false. Vertex isExactlyThisTypeCondition = converter.Solver.And( candidateFunctions.Select((typeCondition, ordinal) => ordinal == i ? typeCondition : converter.Solver.Not(typeCondition))); // If the above conjunction is satisfiable, it means some row configuration exists producing the type. if (!isExactlyThisTypeCondition.IsZero()) { reachableTypes.Add(this.MappedEntityTypes[i]); } } return reachableTypes; } /// /// Determines which types are produced by this mapping. /// private Set FindUnambiguouslyReachableTypes(DomainConstraintConversionContext converter, Vertex[] mappingConditions) { // For each entity type, create a candidate function that evaluates to true given // discriminator assignments iff. all of that type's conditions evaluate to true. Vertex[] candidateFunctions = new Vertex[this.MappedEntityTypes.Count]; for (int i = 0; i < candidateFunctions.Length; i++) { // Seed the candidate function conjunction with 'true'. Vertex candidateFunction = Vertex.One; for (int j = 0; j < this.NormalizedEntityTypeMappings.Count; j++) { var entityTypeMapping = this.NormalizedEntityTypeMappings[j]; // Determine if this mapping is a positive or negative case for the current type. if (entityTypeMapping.ImpliedEntityTypes[i]) { candidateFunction = converter.Solver.And(candidateFunction, mappingConditions[j]); } } candidateFunctions[i] = candidateFunction; } // Make sure that for each type with satisfiable candidateFunction all assignments for the type resolve to only that type. var unambigouslyReachableMap = new BitArray(candidateFunctions.Length, true); for (int i = 0; i < candidateFunctions.Length; ++i) { if (candidateFunctions[i].IsZero()) { // The i-th type is unreachable regardless of other types. unambigouslyReachableMap[i] = false; } else { for (int j = i + 1; j < candidateFunctions.Length; ++j) { if (!converter.Solver.And(candidateFunctions[i], candidateFunctions[j]).IsZero()) { // The i-th and j-th types have common assignments, hence they aren't unambiguously reachable. unambigouslyReachableMap[i] = false; unambigouslyReachableMap[j] = false; } } } } var reachableTypes = new Set(); for (int i = 0; i < candidateFunctions.Length; ++i) { if (unambigouslyReachableMap[i]) { reachableTypes.Add(this.MappedEntityTypes[i]); } } return reachableTypes; } private void CollectUnreachableTypes(Set reachableTypes, out KeyToListMap entityTypes, out KeyToListMap isTypeOfEntityTypes) { // Collect line infos for types in violation entityTypes = new KeyToListMap(EqualityComparer.Default); isTypeOfEntityTypes = new KeyToListMap(EqualityComparer.Default); if (reachableTypes.Count == this.MappedEntityTypes.Count) { // All types are reachable; nothing to check return; } // Find IsTypeOf mappings where no type in hierarchy can generate a row foreach (var isTypeOf in m_isTypeOfLineInfos.Keys) { if (!MetadataHelper.GetTypeAndSubtypesOf(isTypeOf, m_itemCollection, false) .Cast() .Intersect(reachableTypes) .Any()) { // no type in the hierarchy is reachable... isTypeOfEntityTypes.AddRange(isTypeOf, m_isTypeOfLineInfos.EnumerateValues(isTypeOf)); } } // Find explicit types not generating a value foreach (var entityType in m_entityTypeLineInfos.Keys) { if (!reachableTypes.Contains(entityType)) { entityTypes.AddRange(entityType, m_entityTypeLineInfos.EnumerateValues(entityType)); } } } } internal sealed class FunctionImportNormalizedEntityTypeMapping { internal FunctionImportNormalizedEntityTypeMapping(FunctionImportStructuralTypeMappingKB parent, List columnConditions, BitArray impliedEntityTypes) { // validate arguments EntityUtil.CheckArgumentNull(parent, "parent"); EntityUtil.CheckArgumentNull(columnConditions, "discriminatorValues"); EntityUtil.CheckArgumentNull(impliedEntityTypes, "impliedEntityTypes"); Debug.Assert(columnConditions.Count == parent.DiscriminatorColumns.Count, "discriminator values must be ordinally aligned with discriminator columns"); Debug.Assert(impliedEntityTypes.Count == parent.MappedEntityTypes.Count, "implied entity types must be ordinally aligned with mapped entity types"); this.ColumnConditions = new OM.ReadOnlyCollection(columnConditions.ToList()); this.ImpliedEntityTypes = impliedEntityTypes; this.ComplementImpliedEntityTypes = (new BitArray(this.ImpliedEntityTypes)).Not(); } /// /// Gets discriminator values aligned with DiscriminatorColumns of the parent FunctionImportMapping. /// A null ValueCondition indicates 'anything goes'. /// internal readonly OM.ReadOnlyCollection ColumnConditions; /// /// Gets bit array with 'true' indicating the corresponding MappedEntityType of the parent /// FunctionImportMapping is implied by this fragment. /// internal readonly BitArray ImpliedEntityTypes; /// /// Gets the complement of the ImpliedEntityTypes BitArray. /// internal readonly BitArray ComplementImpliedEntityTypes; public override string ToString() { return String.Format(CultureInfo.InvariantCulture, "Values={0}, Types={1}", StringUtil.ToCommaSeparatedString(this.ColumnConditions), StringUtil.ToCommaSeparatedString(this.ImpliedEntityTypes)); } } internal abstract class FunctionImportEntityTypeMappingCondition { protected FunctionImportEntityTypeMappingCondition(string columnName, LineInfo lineInfo) { this.ColumnName = EntityUtil.CheckArgumentNull(columnName, "columnName"); this.LineInfo = lineInfo; } internal readonly string ColumnName; internal readonly LineInfo LineInfo; internal abstract ValueCondition ConditionValue { get; } internal abstract bool ColumnValueMatchesCondition(object columnValue); public override string ToString() { return this.ConditionValue.ToString(); } } internal sealed class FunctionImportEntityTypeMappingConditionValue : FunctionImportEntityTypeMappingCondition { internal FunctionImportEntityTypeMappingConditionValue(string columnName, XPathNavigator columnValue, LineInfo lineInfo) : base(columnName, lineInfo) { this._xPathValue = EntityUtil.CheckArgumentNull(columnValue, "columnValue"); this._convertedValues = new Memoizer(this.GetConditionValue, null); } private readonly XPathNavigator _xPathValue; private readonly Memoizer _convertedValues; internal override ValueCondition ConditionValue { get { return new ValueCondition(_xPathValue.Value); } } internal override bool ColumnValueMatchesCondition(object columnValue) { if (null == columnValue || Convert.IsDBNull(columnValue)) { // only FunctionImportEntityTypeMappingConditionIsNull can match a null // column value return false; } Type columnValueType = columnValue.GetType(); // check if we've interpreted this column type yet object conditionValue = _convertedValues.Evaluate(columnValueType); return ByValueEqualityComparer.Default.Equals(columnValue, conditionValue); } private object GetConditionValue(Type columnValueType) { return GetConditionValue( columnValueType, handleTypeNotComparable: () => { throw EntityUtil.CommandExecution(Strings.Mapping_FunctionImport_UnsupportedType(this.ColumnName, columnValueType.FullName)); }, handleInvalidConditionValue: () => { throw EntityUtil.CommandExecution(Strings.Mapping_FunctionImport_ConditionValueTypeMismatch(StorageMslConstructs.FunctionImportMappingElement, this.ColumnName, columnValueType.FullName)); }); } internal object GetConditionValue(Type columnValueType, Action handleTypeNotComparable, Action handleInvalidConditionValue) { // Check that the type is supported and comparable. PrimitiveType primitiveType; if (!ClrProviderManifest.Instance.TryGetPrimitiveType(columnValueType, out primitiveType) || !StorageMappingItemLoader.IsTypeSupportedForCondition(primitiveType.PrimitiveTypeKind)) { handleTypeNotComparable(); return null; } try { return _xPathValue.ValueAs(columnValueType); } catch (FormatException) { handleInvalidConditionValue(); return null; } } } internal sealed class FunctionImportEntityTypeMappingConditionIsNull : FunctionImportEntityTypeMappingCondition { internal FunctionImportEntityTypeMappingConditionIsNull(string columnName, bool isNull, LineInfo lineInfo) : base(columnName, lineInfo) { this.IsNull = isNull; } internal readonly bool IsNull; internal override ValueCondition ConditionValue { get { return IsNull ? ValueCondition.IsNull : ValueCondition.IsNotNull; } } internal override bool ColumnValueMatchesCondition(object columnValue) { bool valueIsNull = null == columnValue || Convert.IsDBNull(columnValue); return valueIsNull == this.IsNull; } } /// /// Represents a simple value condition of the form (value IS NULL), (value IS NOT NULL) /// or (value EQ X). Supports IEquatable(Of ValueCondition) so that equivalent conditions /// can be identified. /// internal class ValueCondition : IEquatable { internal readonly string Description; internal readonly bool IsSentinel; internal const string IsNullDescription = "NULL"; internal const string IsNotNullDescription = "NOT NULL"; internal const string IsOtherDescription = "OTHER"; internal readonly static ValueCondition IsNull = new ValueCondition(IsNullDescription, true); internal readonly static ValueCondition IsNotNull = new ValueCondition(IsNotNullDescription, true); internal readonly static ValueCondition IsOther = new ValueCondition(IsOtherDescription, true); private ValueCondition(string description, bool isSentinel) { Description = description; IsSentinel = isSentinel; } internal ValueCondition(string description) : this(description, false) { } internal bool IsNotNullCondition { get { return object.ReferenceEquals(this, IsNotNull); } } public bool Equals(ValueCondition other) { return other.IsSentinel == this.IsSentinel && other.Description == this.Description; } public override int GetHashCode() { return Description.GetHashCode(); } public override string ToString() { return this.Description; } } internal sealed class LineInfo: IXmlLineInfo { private readonly bool m_hasLineInfo; private readonly int m_lineNumber; private readonly int m_linePosition; internal LineInfo(XPathNavigator nav) : this((IXmlLineInfo)nav) { } internal LineInfo(IXmlLineInfo lineInfo) { m_hasLineInfo = lineInfo.HasLineInfo(); m_lineNumber = lineInfo.LineNumber; m_linePosition = lineInfo.LinePosition; } internal static readonly LineInfo Empty = new LineInfo(); private LineInfo() { m_hasLineInfo = false; m_lineNumber = default(int); m_linePosition = default(int); } public int LineNumber { get { return m_lineNumber; } } public int LinePosition { get { return m_linePosition; } } public bool HasLineInfo() { return m_hasLineInfo; } } }