/// <summary>Walks the subtree of this node and removes all nodes which were not marked projected.</summary> /// <remarks>Used to remove unnecessary expanded nodes.</remarks> internal void RemoveNonProjectedNodes() { for (int j = this.nodes.Count - 1; j >= 0; j--) { ExpandedProjectionNode expandedNode = this.nodes[j] as ExpandedProjectionNode; // Leave non-expanded properties there as they specify projections. if (expandedNode == null) { continue; } // If we are to project entire subtree, leave all expanded nodes in as well // otherwise remove the expanded nodes which are not marked as projected. if (!this.projectSubtree && !expandedNode.ProjectionFound) { // This removes the expandedNode from the tree (and all its children) this.nodes.RemoveAt(j); } else { expandedNode.RemoveNonProjectedNodes(); } } }
/// <summary>Marks the entire subtree as projected.</summary> /// <remarks>This is used when there were no projections specified in the query /// to mark the entire tree as projected.</remarks> internal void MarkSubtreeAsProjected() { this.projectSubtree = true; this.projectAllImmediateProperties = false; foreach (ProjectionNode node in this.nodes) { ExpandedProjectionNode expandedNode = node as ExpandedProjectionNode; if (expandedNode != null) { expandedNode.MarkSubtreeAsProjected(); } } }
/// <summary> /// Add the projection node with the given property name, property instance and target resource type, if required. /// </summary> /// <param name="propertyName">Name of the property that needs to be projected.</param> /// <param name="property">ResourceProperty instance containing information about the property - this will be null for open properties.</param> /// <param name="targetResourceType">ResourceType instance on which the property needs to be expanded.</param> /// <returns>A new or an existing instance of ProjectionNode.</returns> internal ExpandedProjectionNode AddProjectionNode(string propertyName, ResourceProperty property, ResourceType targetResourceType) { // find out if there exists another node with the given property name. ProjectionNode node = this.FindNode(propertyName); if (node == null || !ExpandedProjectionNode.ApplyPropertyToExistingNode(node, property, targetResourceType)) { // just create a simple projection node for it node = new ProjectionNode(propertyName, property, targetResourceType); this.AddNode(node); } return(node as ExpandedProjectionNode); }
/// <summary> /// Whether the given property can be applied to the existing node. /// </summary> /// <param name="existingNode">Existing node with the same property name.</param> /// <param name="property">ResourceProperty instance for the property refered by the new segment.</param> /// <param name="targetResourceType">TargetResourceType for the new segment.</param> /// <returns>true if the given property can be applied to the existing node, otherwise returns false.</returns> /// <remarks>In case this function returns true, it might modify the TargetResourceType property of the existing node.</remarks> private static bool ApplyPropertyToExistingNode(ProjectionNode existingNode, ResourceProperty property, ResourceType targetResourceType) { Debug.Assert(existingNode != null, "existingNode != null"); Debug.Assert(property == null || property.Name == existingNode.PropertyName, "property == null || property.Name == existingNode.PropertyName"); // This is just quicker way of determining if declared property belongs to different sub-trees. // Since any given property can be declared once in a chain of derived types, if the property instances // are not the same, we can assume that they do not belong to the same hierarchy chain. // Even if we do not have this check, everything will just work, since we check the type assignability. if (property != null && existingNode.Property != null && property != existingNode.Property) { return(false); } ExpandedProjectionNode expansionNode = existingNode as ExpandedProjectionNode; // If the target resource type of the new segment is a super type, we might // need to update the target resource type of the existing node. // Since IsAssignableFrom returns true when they are equal, it isn't require // to do anything, but if we do, it should still work. if (targetResourceType.IsAssignableFrom(existingNode.TargetResourceType)) { ExpandedProjectionNode.VerifyPropertyMismatchAndExpandSelectMismatchScenario( existingNode, property, targetResourceType, expansionNode != null); existingNode.TargetResourceType = targetResourceType; return(true); } // If the existing node is already a super type, then there is nothing to do // other than validating the error scenarios. if (existingNode.TargetResourceType.IsAssignableFrom(targetResourceType)) { ExpandedProjectionNode.VerifyPropertyMismatchAndExpandSelectMismatchScenario( existingNode, property, targetResourceType, expansionNode != null); return(true); } return(false); }
/// <summary> /// Adds a new expanded node with the given segment name, if required. /// </summary> /// <param name="segment">Expand segment as specified in the uri.</param> /// <param name="childSelectExpandClause">The select expand clause for the current node from the URI Parser.</param> /// <returns>An instance of ExpandedProjectionNode - either new or existing one.</returns> internal ExpandedProjectionNode AddExpandedNode(ExpandSegment segment, SelectExpandClause childSelectExpandClause) { Debug.Assert(segment.ExpandedProperty != null, "we do not allow expands on open properties"); Debug.Assert(childSelectExpandClause != null, "childSelectExpandClause != null"); ExpandedProjectionNode childNode = (ExpandedProjectionNode)this.FindNode(segment.Name); // Currently, since we do not have a back pointer in ResourceProperty for the declaring type, // the same resource property instance can belong to 2 completely different types. // Hence doing reference equality on Resource Property instance does not gaurantee that they // refer to the same property. But if the references are not equal, they are gauranteed to be // different. The check of reference equality is to make sure we do the expensive IsAssignableFrom // check, only if the instances are the same, and if the IsAssignable returns true, then we are // gauranteed that the property refers to the same property. if (childNode != null && childNode.Property == segment.ExpandedProperty) { // If the given segment specifies the same property on a super type (or an ancestor), // then we need to change the target type on the segment to the super type (or ancestor), // otherwise we should ignore the segment. // Since IsAssignableFrom returns true, if the instances are the same, we will update the // target resource type to the same instance. It doesn't matter, but having another check // for reference equality makes code less readable. if (segment.TargetResourceType.IsAssignableFrom(childNode.TargetResourceType)) { childNode.TargetResourceType = segment.TargetResourceType; } } else { childNode = new ExpandedProjectionNode( segment.Name, segment.ExpandedProperty, segment.TargetResourceType, segment.Container, segment.OrderingInfo, segment.Filter, null, segment.Container.PageSize != 0 ? (int?)segment.Container.PageSize : null, segment.MaxResultsExpected != Int32.MaxValue ? (int?)segment.MaxResultsExpected : null, childSelectExpandClause); this.AddNode(childNode); } return(childNode); }
/// <summary>Adds a new child node to this node.</summary> /// <param name="node">The child node to add.</param> private void AddNode(ProjectionNode node) { Debug.Assert(node != null, "node != null"); Debug.Assert( this.nodes.Count( n => n.PropertyName == node.PropertyName && (n.TargetResourceType.IsAssignableFrom(node.TargetResourceType) || node.TargetResourceType.IsAssignableFrom(n.TargetResourceType))) == 0, "make sure there is no node with the same property name in the inheritance hierarchy"); this.nodes.Add(node); ExpandedProjectionNode expandedNode = node as ExpandedProjectionNode; if (expandedNode != null) { this.hasExpandedPropertyOnDerivedType = this.hasExpandedPropertyOnDerivedType || (!ResourceType.CompareReferences(this.ResourceType, node.TargetResourceType)); } }
/// <summary> /// Adds a new expanded node with the given segment name, if required. /// </summary> /// <param name="segment">Expand segment as specified in the uri.</param> /// <param name="childSelectExpandClause">The select expand clause for the current node from the URI Parser.</param> /// <returns>An instance of ExpandedProjectionNode - either new or existing one.</returns> internal ExpandedProjectionNode AddExpandedNode(ExpandSegment segment, SelectExpandClause childSelectExpandClause) { Debug.Assert(segment.ExpandedProperty != null, "we do not allow expands on open properties"); Debug.Assert(childSelectExpandClause != null, "childSelectExpandClause != null"); ExpandedProjectionNode childNode = (ExpandedProjectionNode)this.FindNode(segment.Name); // Currently, since we do not have a back pointer in ResourceProperty for the declaring type, // the same resource property instance can belong to 2 completely different types. // Hence doing reference equality on Resource Property instance does not gaurantee that they // refer to the same property. But if the references are not equal, they are gauranteed to be // different. The check of reference equality is to make sure we do the expensive IsAssignableFrom // check, only if the instances are the same, and if the IsAssignable returns true, then we are // gauranteed that the property refers to the same property. if (childNode != null && childNode.Property == segment.ExpandedProperty) { // If the given segment specifies the same property on a super type (or an ancestor), // then we need to change the target type on the segment to the super type (or ancestor), // otherwise we should ignore the segment. // Since IsAssignableFrom returns true, if the instances are the same, we will update the // target resource type to the same instance. It doesn't matter, but having another check // for reference equality makes code less readable. if (segment.TargetResourceType.IsAssignableFrom(childNode.TargetResourceType)) { childNode.TargetResourceType = segment.TargetResourceType; } } else { childNode = new ExpandedProjectionNode( segment.Name, segment.ExpandedProperty, segment.TargetResourceType, segment.Container, segment.OrderingInfo, segment.Filter, null, segment.Container.PageSize != 0 ? (int?)segment.Container.PageSize : null, segment.MaxResultsExpected != Int32.MaxValue ? (int?)segment.MaxResultsExpected : null, childSelectExpandClause); this.AddNode(childNode); } return childNode; }
/// <summary> /// Applies projections from the given select/expand clause to the tree represented by the given node. /// </summary> /// <param name="currentNode">The expand tree to apply projections to.</param> private void ApplyProjectionsToExpandTree(ExpandedProjectionNode currentNode) { // If this is the last segment in the path and it was a navigation property, // mark it to include the entire subtree if (currentNode.SelectExpandClause.AllSelected) { currentNode.ProjectionFound = true; currentNode.MarkSubtreeAsProjected(); } foreach (var selectItem in currentNode.SelectExpandClause.SelectedItems) { currentNode.ProjectionFound = true; // '*' is special, it means "Project all immediate properties on this level." if (selectItem is WildcardSelectItem) { currentNode.ProjectAllImmediateProperties = true; continue; } if (selectItem is NamespaceQualifiedWildcardSelectItem) { currentNode.ProjectAllImmediateOperations = true; continue; } ODataPath path; var expandItem = selectItem as ExpandedNavigationSelectItem; if (expandItem != null) { path = expandItem.PathToNavigationProperty; } else { var pathSelectionItem = selectItem as PathSelectItem; Debug.Assert(pathSelectionItem != null, "Unexpeced selection item type: " + selectItem.GetType()); path = pathSelectionItem.SelectedPath; } ExpandedProjectionNode childNode = this.ApplyPathSelection(path, currentNode); if (expandItem != null) { if (childNode == null) { throw DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_ProjectedPropertyWithoutMatchingExpand(((NavigationPropertySegment)path.LastSegment).NavigationProperty.Name)); } this.ApplyProjectionsToExpandTree(childNode); } } }
/// <summary> /// Performs a depth-first walk down the expand tree, copying and adding to the current path as it goes. When the bottom of a path is reached, the path is added to the overall set of paths. /// </summary> /// <param name="currentPath">The current path so far for this depth-first traversal of the tree. Starts out null.</param> /// <param name="currentNode">The current node of the expand tree.</param> /// <param name="rootNode">The root node of the expand tree.</param> private void ExtractExpandPathSegmentCollections(ExpandSegmentCollection currentPath, ExpandedProjectionNode currentNode, RootProjectionNode rootNode) { ExpandSegmentCollection nextPath = null; foreach (ExpandedNavigationSelectItem expandItem in currentNode.SelectExpandClause.SelectedItems.OfType<ExpandedNavigationSelectItem>()) { rootNode.ExpansionsSpecified = true; if (currentPath == null) { nextPath = new ExpandSegmentCollection(); } else { // create a copy of the current path. nextPath = new ExpandSegmentCollection(currentPath.Count); nextPath.AddRange(currentPath); } var segment = this.CreateExpandSegment(expandItem, currentNode.ResourceType); nextPath.Add(segment); ExpandedProjectionNode childNode = currentNode.AddExpandedNode(segment, expandItem.SelectAndExpand); // if there are any derived expansions in the tree, set the rootNode.DerivedExpansionsSpecified to true. if (currentNode.HasExpandedPropertyOnDerivedType) { rootNode.ExpansionOnDerivedTypesSpecified = true; } this.ExtractExpandPathSegmentCollections(nextPath, childNode, rootNode); } if (nextPath == null && currentPath != null) { this.expandPaths.Add(currentPath); } }
/// <summary> /// Applies the given path-based selection item to the given projection node. /// </summary> /// <param name="path">The path being selected.</param> /// <param name="currentNode">The current projection node.</param> /// <returns>A new or an existing instance of the ExpandedProjectionNode for path of the selection item.</returns> private ExpandedProjectionNode ApplyPathSelection(ODataPath path, ExpandedProjectionNode currentNode) { Debug.Assert(path != null, "path != null"); ResourceType targetResourceType = this.GetTargetResourceTypeFromTypeSegments(path, currentNode.ResourceType); Debug.Assert(targetResourceType != null, "targetResourceType != null"); ODataPathSegment lastSegment = path.LastSegment; Debug.Assert(lastSegment != null, "lastSegment != null"); var operationSegment = lastSegment as OperationSegment; if (operationSegment != null) { ApplySelectionForOperations(operationSegment, currentNode, targetResourceType); return currentNode; } var openPropertySegment = lastSegment as OpenPropertySegment; if (openPropertySegment != null) { return ApplyProjectionForProperty(currentNode, openPropertySegment.PropertyName, null /*property*/, targetResourceType); } var propertySegment = lastSegment as PropertySegment; IEdmProperty edmProperty; if (propertySegment == null) { var navigationSegment = lastSegment as NavigationPropertySegment; Debug.Assert(navigationSegment != null, "Unexpected type of select segment: " + lastSegment.GetType()); edmProperty = navigationSegment.NavigationProperty; } else { edmProperty = propertySegment.Property; } ResourceProperty resourceProperty = ((IResourcePropertyBasedEdmProperty)edmProperty).ResourceProperty; Debug.Assert(resourceProperty != null, "resourceProperty != null"); // Look for any navigation properties that are hidden (EntitySetRights set to None) // If it is hidden, throw exception so we don't do any information disclousure // See Information disclosure: Astoria server allows 'hidden' navigation properties to be selected if (edmProperty.PropertyKind == EdmPropertyKind.Navigation && this.service.Provider.GetResourceSet(currentNode.ResourceSetWrapper, targetResourceType, resourceProperty) == null) { throw DataServiceException.CreateBadRequestError(Strings.BadRequest_InvalidPropertyNameSpecified(resourceProperty.Name, targetResourceType.FullName)); } return ApplyProjectionForProperty(currentNode, resourceProperty.Name, resourceProperty, targetResourceType); }
/// <summary> /// Applies the selection for the given operation segment to the given projection node. /// </summary> /// <param name="operationSegment">The operation segment to apply to the projection node.</param> /// <param name="currentNode">The current projection node.</param> /// <param name="targetResourceType">The target type based on type segments.</param> private static void ApplySelectionForOperations(OperationSegment operationSegment, ExpandedProjectionNode currentNode, ResourceType targetResourceType) { IEnumerable<OperationWrapper> selectedOperations = operationSegment.Operations.Select(f => { var metadataProviderEdmFunctionImport = f as MetadataProviderEdmOperation; Debug.Assert(metadataProviderEdmFunctionImport != null, "metadataProviderEdmFunctionImport != null"); OperationWrapper operation = metadataProviderEdmFunctionImport.ServiceOperation; Debug.Assert(operation != null, "operation != null"); return operation; }); // Note that AddSelectedOperations will return false if the enumerable is empty. bool anyOperations = currentNode.SelectedOperations.AddSelectedOperations(targetResourceType, selectedOperations); Debug.Assert(anyOperations, "Operations segment should not have been created if no operations were found."); }
/// <summary> /// Apply projection for the given property to the parent projection node. /// </summary> /// <param name="parentNode">The parent node which the new node will be added to.</param> /// <param name="propertyName">Name of the property that needs to be projected.</param> /// <param name="property">ResourceProperty instance containing information about the property - this will be null for open properties.</param> /// <param name="targetResourceType">ResourceType instance on which the property needs to be expanded.</param> /// <returns>A new or an existing instance of the ExpandedProjectionNode for the given property.</returns> private static ExpandedProjectionNode ApplyProjectionForProperty(ExpandedProjectionNode parentNode, string propertyName, ResourceProperty property, ResourceType targetResourceType) { Debug.Assert(parentNode != null, "parentNode != null"); Debug.Assert(!string.IsNullOrEmpty(propertyName), "!string.IsNullOrEmpty(propertyName)"); Debug.Assert(targetResourceType != null, "targetResourceType != null"); var childNode = parentNode.AddProjectionNode(propertyName, property, targetResourceType); Debug.Assert( childNode == null || childNode.ResourceType == property.ResourceType, "If we're traversing over a nav. property it's resource type must match the resource type of the expanded segment."); return childNode; }
/// <summary>Removes duplicates from the tree caused by wildcards and sorts the projected properties.</summary> /// <param name="provider">underlying provider instance.</param> /// <remarks> /// Examples /// $select=Orders, Orders/ID - get rid of the Orders/ID /// $select=Orders, Orders/* - get rid of the Orders/* /// $select=Orders/*, Orders/ID - get rid of the Orders/ID /// $select=Orders/*, Orders/OrderItems&$expand=Orders - get rid of the Orders/OrderItems (it's redundant to *) /// $select=Orders/*, Orders/OrderItems&$expand=Orders/OrderItems - leave as is, the Orders/OrderItems are expanded /// /// The sorting order is the same as the order in which the properties are enumerated on the owning type. /// This is to preserve the same order as if no projections occured. /// </remarks> internal void ApplyWildcardsAndSort(DataServiceProviderWrapper provider) { // If this segment was marked to include entire subtree // simply remove all children which are not expanded // and propagate the information to all expanded children. if (this.projectSubtree) { for (int j = this.nodes.Count - 1; j >= 0; j--) { ExpandedProjectionNode expandedNode = this.nodes[j] as ExpandedProjectionNode; if (expandedNode != null) { expandedNode.projectSubtree = true; expandedNode.ApplyWildcardsAndSort(provider); } } this.projectAllImmediateProperties = false; this.projectAllImmediateOperations = false; return; } for (int j = this.nodes.Count - 1; j >= 0; j--) { ExpandedProjectionNode expandedNode = this.nodes[j] as ExpandedProjectionNode; // If this node was marked to include all immediate properties, // remove all children which are not expanded. // That means they are either simple properties or nav. properties which // are not going to be expanded anyway. if (this.ProjectAllImmediateProperties && expandedNode == null) { this.nodes.RemoveAt(j); } else if (expandedNode != null) { expandedNode.ApplyWildcardsAndSort(provider); } } if (this.nodes.Count > 0) { // Sort the subsegments such that they have the same order as the properties // on the owning resource type. // build the list of existing resource types that this query touches List <ResourceType> resourceTypes = new List <ResourceType>(); resourceTypes.Add(this.ResourceType); // If we have one or more derived properties to expand or project, // we need to sort it based on the order in which the derived types // are return. List <ProjectionNode> derivedProjectionNodes = this.nodes.Where(n => !ResourceType.CompareReferences(n.TargetResourceType, this.ResourceType)).ToList(); if (derivedProjectionNodes.Count > 0) { foreach (ResourceType rt in provider.GetDerivedTypes(this.ResourceType)) { if (derivedProjectionNodes.FirstOrDefault(node => node.TargetResourceType == rt) != null) { resourceTypes.Add(rt); } } } #if DEBUG int count = this.nodes.Count; #endif this.nodes = ExpandedProjectionNode.SortNodes(this.nodes, resourceTypes); #if DEBUG Debug.Assert(this.nodes.Count == count, "We didn't sort all the properties."); #endif } }