public void GivenAChainedParameterPointingToASingleResourceType_WhenParsed_ThenCorrectExpressionShouldBeCreated() { ResourceType sourceResourceType = ResourceType.Patient; ResourceType targetResourceType = ResourceType.Organization; string param1 = "ref"; string param2 = "param"; string key = $"{param1}.{param2}"; string value = "Seattle"; // Setup the search parameters. SetupReferenceSearchParameter( sourceResourceType, param1, targetResourceType); SearchParameterInfo searchParameter = SetupSearchParameter(targetResourceType, param2); Expression expectedExpression = SetupExpression(searchParameter, value); // Parse the expression. Expression expression = _expressionParser.Parse(sourceResourceType.ToString(), key, value); ValidateMultiaryExpression( expression, MultiaryOperator.Or, chainedExpression => ValidateChainedExpression( chainedExpression, sourceResourceType, param1, targetResourceType.ToString(), actualSearchExpression => Assert.Equal(expectedExpression, actualSearchExpression))); }
private Expression SetupExpression(SearchParameterInfo searchParameter, string value) { Expression expectedExpression = Substitute.For <Expression>(); _searchParameterExpressionParser.Parse(searchParameter, null, value).Returns(expectedExpression); return(expectedExpression); }
private Expression CreateSearchExpression(ResourceElement coverage, ResourceElement patient) { var coverageValues = _searchIndexer.Extract(coverage); var patientValues = _searchIndexer.Extract(patient); var expressions = new List <Expression>(); var reverseChainExpressions = new List <Expression>(); expressions.Add(Expression.SearchParameter(_resourceTypeSearchParameter, Expression.StringEquals(FieldName.TokenCode, null, KnownResourceTypes.Patient, false))); foreach (var patientValue in patientValues) { if (IgnoreInSearch(patientValue)) { continue; } var modifier = string.Empty; if (patientValue.SearchParameter.Type == ValueSets.SearchParamType.String) { modifier = ":exact"; } expressions.Add(_expressionParser.Parse(new[] { KnownResourceTypes.Patient }, patientValue.SearchParameter.Code + modifier, patientValue.Value.ToString())); } foreach (var coverageValue in coverageValues) { if (IgnoreInSearch(coverageValue)) { continue; } var modifier = string.Empty; if (coverageValue.SearchParameter.Type == ValueSets.SearchParamType.String) { modifier = ":exact"; } reverseChainExpressions.Add(_expressionParser.Parse(new[] { KnownResourceTypes.Coverage }, coverageValue.SearchParameter.Code + modifier, coverageValue.Value.ToString())); } if (reverseChainExpressions.Count != 0) { Expression reverseChainedExpression; if (reverseChainExpressions.Count == 1) { reverseChainedExpression = reverseChainExpressions[0]; } else { reverseChainedExpression = Expression.And(reverseChainExpressions); } var expression = Expression.Chained(new[] { KnownResourceTypes.Coverage }, _coverageBeneficiaryParameter, new[] { KnownResourceTypes.Patient }, true, reverseChainedExpression); expressions.Add(expression); } return(Expression.And(expressions)); }
public void GivenMissingModifierIsSpecified_WhenBuilt_ThenMissingExpressionShouldBeCreated(string isMissingString, bool expectedIsMissing) { Expression expression = _parser.Parse( CreateSearchParameter(SearchParamType.String), new SearchModifier(SearchModifierCode.Missing), isMissingString); ValidateMissingParamExpression(expression, DefaultParamName, expectedIsMissing); }
private void Validate( SearchParameterInfo searchParameter, SearchModifier modifier, string value, Action <Expression> valueValidator) { Expression expression = _parser.Parse(searchParameter, modifier, value); Assert.NotNull(expression); ValidateSearchParameterExpression(expression, DefaultParamName, valueValidator); }
public void GivenAChainedParameterPointingToMultipleResourceTypes_WhenParsed_ThenCorrectExpressionShouldBeCreated() { ResourceType sourceResourceType = ResourceType.Patient; ResourceType[] targetResourceTypes = new[] { ResourceType.Organization, ResourceType.Practitioner }; string param1 = "ref"; string param2 = "param"; string key = $"{param1}.{param2}"; string value = "Seattle"; // Setup the search parameters. SetupReferenceSearchParameter(sourceResourceType, param1, targetResourceTypes); var expectedTargets = targetResourceTypes.Select(targetResourceType => { SearchParameterInfo searchParameter = SetupSearchParameter(targetResourceType, param2); Expression expectedExpression = SetupExpression(searchParameter, value); return(new { TargetResourceType = targetResourceType, Expression = expectedExpression }); }) .ToArray(); // Parse the expression. Expression expression = _expressionParser.Parse(sourceResourceType.ToString(), key, value); ValidateMultiaryExpression( expression, MultiaryOperator.Or, expectedTargets.Select(expected => { return((Action <Expression>)(chainedExpression => ValidateChainedExpression( chainedExpression, sourceResourceType, param1, expected.TargetResourceType.ToString(), actualSearchExpression => Assert.Equal(expected.Expression, actualSearchExpression)))); }) .ToArray()); }
public void GivenANestedChainedParameter_WhenParsed_ThenCorrectExpressionShouldBeCreated() { ResourceType sourceResourceType = ResourceType.Patient; ResourceType firstTargetResourceType = ResourceType.Organization; ResourceType secondTargetResourceType = ResourceType.Practitioner; string param1 = "ref1"; string param2 = "ref2"; string param3 = "param"; string key = $"{param1}.{param2}.{param3}"; string value = "Microsoft"; // Setup the search parameters. SetupReferenceSearchParameter(sourceResourceType, param1, firstTargetResourceType); SetupReferenceSearchParameter(firstTargetResourceType, param2, secondTargetResourceType); SearchParameterInfo searchParameter = SetupSearchParameter(secondTargetResourceType, param3); Expression expectedExpression = SetupExpression(searchParameter, value); // Parse the expression. Expression expression = _expressionParser.Parse(sourceResourceType.ToString(), key, value); ValidateMultiaryExpression( expression, MultiaryOperator.Or, chainedExpression => ValidateChainedExpression( chainedExpression, sourceResourceType, param1, firstTargetResourceType.ToString(), nestedExpression => ValidateMultiaryExpression( nestedExpression, MultiaryOperator.Or, nestedChainedExpression => ValidateChainedExpression( nestedChainedExpression, firstTargetResourceType, param2, secondTargetResourceType.ToString(), actualSearchExpression => Assert.Equal(expectedExpression, actualSearchExpression))))); }
public void GivenAChainedParameterPointingToMultipleResourceTypesAndSearchParamIsNotSupportedByAllTargetResourceTypes_WhenParsed_ThenOnlyExpressionsForResourceTypeThatSupportsSearchParamShouldBeCreated() { ResourceType sourceResourceType = ResourceType.Patient; // The reference will support both Organization and Practitioner, // but the search value will only be supported by Practitioner. ResourceType[] targetResourceTypes = new[] { ResourceType.Organization, ResourceType.Practitioner }; string param1 = "ref"; string param2 = "param"; string key = $"{param1}.{param2}"; string value = "Lewis"; // Setup the search parameters. SetupReferenceSearchParameter(sourceResourceType, param1, targetResourceTypes); // Setup the Organization to not support this search param. _searchParameterDefinitionManager.GetSearchParameter(ResourceType.Organization.ToString(), param2) .Returns(x => throw new SearchParameterNotSupportedException(x.ArgAt <string>(0), x.ArgAt <string>(1))); // Setup the Practitioner to support this search param. SearchParameterInfo searchParameter = SetupSearchParameter(ResourceType.Practitioner, param2); Expression expectedExpression = SetupExpression(searchParameter, value); // Parse the expression. Expression expression = _expressionParser.Parse(sourceResourceType.ToString(), key, value); ValidateMultiaryExpression( expression, MultiaryOperator.Or, chainedExpression => ValidateChainedExpression( chainedExpression, sourceResourceType, param1, ResourceType.Practitioner.ToString(), actualSearchExpression => Assert.Equal(expectedExpression, actualSearchExpression))); }
public void GivenAChainedParameterPointingToMultipleResourceTypesAndWithResourceTypeSpecified_WhenParsed_ThenOnlyExpressionForTheSpecifiedResourceTypeShouldBeCreated() { ResourceType sourceResourceType = ResourceType.Patient; // The reference will support both Organization and Practitioner, // but we will limit the search to Organization only in the key below. ResourceType[] targetResourceTypes = new[] { ResourceType.Organization, ResourceType.Practitioner }; string param1 = "ref"; string param2 = "param"; string key = $"{param1}:Organization.{param2}"; string value = "Seattle"; // Setup the search parameters. SetupReferenceSearchParameter(sourceResourceType, param1, targetResourceTypes); Expression[] expectedExpressions = targetResourceTypes.Select(targetResourceType => { SearchParameterInfo searchParameter = SetupSearchParameter(targetResourceType, param2); return(SetupExpression(searchParameter, value)); }) .ToArray(); // Parse the expression. Expression expression = _expressionParser.Parse(sourceResourceType.ToString(), key, value); ValidateMultiaryExpression( expression, MultiaryOperator.Or, chainedExpression => ValidateChainedExpression( chainedExpression, sourceResourceType, param1, ResourceType.Organization.ToString(), actualSearchExpression => Assert.Equal(expectedExpressions[0], actualSearchExpression))); }
public void GivenAModifier_WhenParsed_ThenExceptionShouldBeThrown() { ResourceType resourceType = ResourceType.Patient; string param1 = "ref"; string modifier = "missing"; // Practitioner is a valid resource type but is not supported by the search parameter. string key = $"{param1}:{modifier}"; string value = "Seattle"; SearchParameterInfo searchParameter = SetupSearchParameter(resourceType, param1); Expression expression = Substitute.For <Expression>(); _searchParameterExpressionParser.Parse(searchParameter, SearchParameter.SearchModifierCode.Missing, value).Returns(expression); // Parse the expression. Expression actualExpression = _expressionParser.Parse(resourceType.ToString(), key, value); // The mock requires the modifier to match so if we get the same expression instance // then it means we got the modifier correctly. Assert.Equal(expression, actualExpression); }
public SearchOptions Create(string compartmentType, string compartmentId, string resourceType, IReadOnlyList <Tuple <string, string> > queryParameters) { var searchOptions = new SearchOptions(); string continuationToken = null; var searchParams = new SearchParams(); var unsupportedSearchParameters = new List <Tuple <string, string> >(); // Extract the continuation token, filter out the other known query parameters that's not search related. foreach (Tuple <string, string> query in queryParameters ?? Enumerable.Empty <Tuple <string, string> >()) { if (query.Item1 == KnownQueryParameterNames.ContinuationToken) { if (continuationToken != null) { throw new InvalidSearchOperationException( string.Format(Core.Resources.MultipleQueryParametersNotAllowed, KnownQueryParameterNames.ContinuationToken)); } continuationToken = query.Item2; } else if (query.Item1 == KnownQueryParameterNames.Format) { // TODO: We need to handle format parameter. } else if (string.IsNullOrWhiteSpace(query.Item1) || string.IsNullOrWhiteSpace(query.Item2)) { // Query parameter with empty value is not supported. unsupportedSearchParameters.Add(query); } else { // Parse the search parameters. try { searchParams.Add(query.Item1, query.Item2); } catch (Exception ex) { _logger.LogInformation(ex, "Failed to parse the query parameter. Skipping."); // There was a problem parsing the parameter. Add it to list of unsupported parameters. unsupportedSearchParameters.Add(query); } } } searchOptions.ContinuationToken = continuationToken; // Check the item count. if (searchParams.Count != null) { searchOptions.MaxItemCount = searchParams.Count.Value; } // Check to see if only the count should be returned searchOptions.CountOnly = searchParams.Summary == SummaryType.Count; // If the resource type is not specified, then the common // search parameters should be used. ResourceType parsedResourceType = ResourceType.DomainResource; if (!string.IsNullOrWhiteSpace(resourceType) && !Enum.TryParse(resourceType, out parsedResourceType)) { throw new ResourceNotSupportedException(resourceType); } var searchExpressions = new List <Expression>(); if (!string.IsNullOrWhiteSpace(resourceType)) { searchExpressions.Add(Expression.SearchParameter(_resourceTypeSearchParameter, Expression.StringEquals(FieldName.TokenCode, null, resourceType, false))); } searchExpressions.AddRange(searchParams.Parameters.Select( q => { try { return(_expressionParser.Parse(parsedResourceType.ToString(), q.Item1, q.Item2)); } catch (SearchParameterNotSupportedException) { unsupportedSearchParameters.Add(q); return(null); } }) .Where(item => item != null)); if (!string.IsNullOrWhiteSpace(compartmentType)) { if (Enum.TryParse(compartmentType, out CompartmentType parsedCompartmentType)) { if (string.IsNullOrWhiteSpace(compartmentId)) { throw new InvalidSearchOperationException(Core.Resources.CompartmentIdIsInvalid); } searchExpressions.Add(Expression.CompartmentSearch(compartmentType, compartmentId)); } else { throw new InvalidSearchOperationException(string.Format(Core.Resources.CompartmentTypeIsInvalid, compartmentType)); } } if (searchExpressions.Count == 1) { searchOptions.Expression = searchExpressions[0]; } else if (searchExpressions.Count > 1) { searchOptions.Expression = Expression.And(searchExpressions.ToArray()); } if (unsupportedSearchParameters.Any()) { // TODO: Client can specify whether exception should be raised or not when it encounters unknown search parameters. // For now, we will ignore any unknown search parameters. } searchOptions.UnsupportedSearchParams = unsupportedSearchParameters; return(searchOptions); }
public SearchOptions Create(string compartmentType, string compartmentId, string resourceType, IReadOnlyList <Tuple <string, string> > queryParameters) { var searchOptions = new SearchOptions(); string continuationToken = null; var searchParams = new SearchParams(); var unsupportedSearchParameters = new List <Tuple <string, string> >(); bool setDefaultBundleTotal = true; // Extract the continuation token, filter out the other known query parameters that's not search related. foreach (Tuple <string, string> query in queryParameters ?? Enumerable.Empty <Tuple <string, string> >()) { if (query.Item1 == KnownQueryParameterNames.ContinuationToken) { // This is an unreachable case. The mapping of the query parameters makes it so only one continuation token can exist. if (continuationToken != null) { throw new InvalidSearchOperationException( string.Format(Core.Resources.MultipleQueryParametersNotAllowed, KnownQueryParameterNames.ContinuationToken)); } try { continuationToken = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(query.Item2)); } catch (FormatException) { throw new BadRequestException(Core.Resources.InvalidContinuationToken); } setDefaultBundleTotal = false; } else if (query.Item1 == KnownQueryParameterNames.Format) { // TODO: We need to handle format parameter. } else if (string.IsNullOrWhiteSpace(query.Item1) || string.IsNullOrWhiteSpace(query.Item2)) { // Query parameter with empty value is not supported. unsupportedSearchParameters.Add(query); } else if (string.Compare(query.Item1, KnownQueryParameterNames.Total, StringComparison.OrdinalIgnoreCase) == 0) { if (Enum.TryParse <TotalType>(query.Item2, true, out var totalType)) { ValidateTotalType(totalType); searchOptions.IncludeTotal = totalType; setDefaultBundleTotal = false; } else { throw new BadRequestException(string.Format(Core.Resources.InvalidTotalParameter, query.Item2, SupportedTotalTypes)); } } else { // Parse the search parameters. try { // Basic format checking (e.g. integer value for _count key etc.). searchParams.Add(query.Item1, query.Item2); } catch (Exception ex) { throw new BadRequestException(ex.Message); } } } searchOptions.ContinuationToken = continuationToken; if (setDefaultBundleTotal) { ValidateTotalType(_featureConfiguration.IncludeTotalInBundle); searchOptions.IncludeTotal = _featureConfiguration.IncludeTotalInBundle; } // Check the item count. if (searchParams.Count != null) { if (searchParams.Count > _featureConfiguration.MaxItemCountPerSearch) { searchOptions.MaxItemCount = _featureConfiguration.MaxItemCountPerSearch; _contextAccessor.FhirRequestContext.BundleIssues.Add( new OperationOutcomeIssue( OperationOutcomeConstants.IssueSeverity.Information, OperationOutcomeConstants.IssueType.Informational, string.Format(Core.Resources.SearchParamaterCountExceedLimit, _featureConfiguration.MaxItemCountPerSearch, searchParams.Count))); } else { searchOptions.MaxItemCount = searchParams.Count.Value; } } else { searchOptions.MaxItemCount = _featureConfiguration.DefaultItemCountPerSearch; } searchOptions.IncludeCount = _featureConfiguration.DefaultIncludeCountPerSearch; // Check to see if only the count should be returned searchOptions.CountOnly = searchParams.Summary == SummaryType.Count; // If the resource type is not specified, then the common // search parameters should be used. ResourceType parsedResourceType = ResourceType.DomainResource; if (!string.IsNullOrWhiteSpace(resourceType) && !Enum.TryParse(resourceType, out parsedResourceType)) { throw new ResourceNotSupportedException(resourceType); } var searchExpressions = new List <Expression>(); if (!string.IsNullOrWhiteSpace(resourceType)) { searchExpressions.Add(Expression.SearchParameter(_resourceTypeSearchParameter, Expression.StringEquals(FieldName.TokenCode, null, resourceType, false))); } searchExpressions.AddRange(searchParams.Parameters.Select( q => { try { return(_expressionParser.Parse(parsedResourceType.ToString(), q.Item1, q.Item2)); } catch (SearchParameterNotSupportedException) { unsupportedSearchParameters.Add(q); return(null); } }) .Where(item => item != null)); if (searchParams.Include?.Count > 0) { searchExpressions.AddRange(searchParams.Include.Select( q => _expressionParser.ParseInclude(parsedResourceType.ToString(), q, false /* not reversed */, false /* no iterate */)) .Where(item => item != null)); } if (searchParams.RevInclude?.Count > 0) { searchExpressions.AddRange(searchParams.RevInclude.Select( q => _expressionParser.ParseInclude(parsedResourceType.ToString(), q, true /* reversed */, false /* no iterate */)) .Where(item => item != null)); } // Parse _include:iterate (_include:recurse) parameters. // :iterate (:recurse) modifiers are not supported by Hl7.Fhir.Rest, hence not added to the Include collection and exist in the Parameters list. // See https://github.com/FirelyTeam/fhir-net-api/issues/222 // _include:iterate (_include:recurse) expression may appear without a preceding _include parameter // when applied on a circular reference searchExpressions.AddRange(ParseIncludeIterateExpressions(searchParams)); // remove _include:iterate and _revinclude:iterate parameters from unsupportedSearchParameters unsupportedSearchParameters.RemoveAll(p => AllIterateModifiers.Contains(p.Item1)); if (!string.IsNullOrWhiteSpace(compartmentType)) { if (Enum.TryParse(compartmentType, out CompartmentType parsedCompartmentType)) { if (string.IsNullOrWhiteSpace(compartmentId)) { throw new InvalidSearchOperationException(Core.Resources.CompartmentIdIsInvalid); } searchExpressions.Add(Expression.CompartmentSearch(compartmentType, compartmentId)); } else { throw new InvalidSearchOperationException(string.Format(Core.Resources.CompartmentTypeIsInvalid, compartmentType)); } } if (searchExpressions.Count == 1) { searchOptions.Expression = searchExpressions[0]; } else if (searchExpressions.Count > 1) { searchOptions.Expression = Expression.And(searchExpressions.ToArray()); } if (unsupportedSearchParameters.Any()) { // TODO: Client can specify whether exception should be raised or not when it encounters unknown search parameters. // For now, we will ignore any unknown search parameters. } searchOptions.UnsupportedSearchParams = unsupportedSearchParameters; if (searchParams.Sort?.Count > 0) { var sortings = new List <(SearchParameterInfo, SortOrder)>(); List <(string parameterName, string reason)> unsupportedSortings = null; foreach (Tuple <string, Hl7.Fhir.Rest.SortOrder> sorting in searchParams.Sort) { try { SearchParameterInfo searchParameterInfo = _searchParameterDefinitionManager.GetSearchParameter(parsedResourceType.ToString(), sorting.Item1); if (searchParameterInfo.IsSortSupported()) { sortings.Add((searchParameterInfo, sorting.Item2.ToCoreSortOrder())); } else { throw new SearchParameterNotSupportedException(string.Format(Core.Resources.SearchSortParameterNotSupported, searchParameterInfo.Name)); } } catch (SearchParameterNotSupportedException) { (unsupportedSortings ??= new List <(string parameterName, string reason)>()).Add((sorting.Item1, string.Format(Core.Resources.SearchSortParameterNotSupported, sorting.Item1))); } } searchOptions.Sort = sortings; searchOptions.UnsupportedSortingParams = (IReadOnlyList <(string parameterName, string reason)>)unsupportedSortings ?? Array.Empty <(string parameterName, string reason)>(); } else { searchOptions.Sort = Array.Empty <(SearchParameterInfo searchParameterInfo, SortOrder sortOrder)>(); searchOptions.UnsupportedSortingParams = Array.Empty <(string parameterName, string reason)>(); } return(searchOptions); IEnumerable <IncludeExpression> ParseIncludeIterateExpressions(SearchParams searchParams) { return(searchParams.Parameters .Where(p => p != null && AllIterateModifiers.Where(m => string.Equals(p.Item1, m, StringComparison.OrdinalIgnoreCase)).Any()) .Select(p => { var includeResourceType = p.Item2?.Split(':')[0]; if (!ModelInfoProvider.IsKnownResource(includeResourceType)) { throw new ResourceNotSupportedException(includeResourceType); } var reversed = RevIncludeIterateModifiers.Contains(p.Item1); var expression = _expressionParser.ParseInclude(includeResourceType, p.Item2, reversed, true); // Reversed Iterate expressions (not wildcard) must specify target type if there is more than one possible target type if (expression.Reversed && expression.Iterate && expression.TargetResourceType == null && expression.ReferenceSearchParameter?.TargetResourceTypes?.Count > 1) { throw new BadRequestException(string.Format(Core.Resources.RevIncludeIterateTargetTypeNotSpecified, p.Item2)); } // For circular include iterate expressions, add an informational issue indicating that a single iteration is supported. // See https://www.hl7.org/fhir/search.html#revinclude. if (expression.Iterate && expression.CircularReference) { _contextAccessor.FhirRequestContext.BundleIssues.Add( new OperationOutcomeIssue( OperationOutcomeConstants.IssueSeverity.Information, OperationOutcomeConstants.IssueType.Informational, string.Format(Core.Resources.IncludeIterateCircularReferenceExecutedOnce, p.Item1, p.Item2))); } return expression; })); } void ValidateTotalType(TotalType totalType) { // Estimate is not yet supported. if (totalType == TotalType.Estimate) { throw new SearchOperationNotSupportedException(string.Format(Core.Resources.UnsupportedTotalParameter, totalType, SupportedTotalTypes)); } } }
public SearchOptions Create(string compartmentType, string compartmentId, string resourceType, IReadOnlyList <Tuple <string, string> > queryParameters) { var searchOptions = new SearchOptions(); string continuationToken = null; var searchParams = new SearchParams(); var unsupportedSearchParameters = new List <Tuple <string, string> >(); bool setDefaultBundleTotal = true; // Extract the continuation token, filter out the other known query parameters that's not search related. foreach (Tuple <string, string> query in queryParameters ?? Enumerable.Empty <Tuple <string, string> >()) { if (query.Item1 == KnownQueryParameterNames.ContinuationToken) { // This is an unreachable case. The mapping of the query parameters makes it so only one continuation token can exist. if (continuationToken != null) { throw new InvalidSearchOperationException( string.Format(Core.Resources.MultipleQueryParametersNotAllowed, KnownQueryParameterNames.ContinuationToken)); } try { continuationToken = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(query.Item2)); } catch (FormatException) { throw new BadRequestException(Core.Resources.InvalidContinuationToken); } setDefaultBundleTotal = false; } else if (query.Item1 == KnownQueryParameterNames.Format || query.Item1 == KnownQueryParameterNames.Pretty) { // _format and _pretty are not search parameters, so we can ignore them. } else if (string.Equals(query.Item1, KnownQueryParameterNames.Type, StringComparison.OrdinalIgnoreCase)) { if (string.IsNullOrWhiteSpace(query.Item2)) { throw new BadRequestException(string.Format(Core.Resources.InvalidTypeParameter, query.Item2)); } var types = query.Item2.SplitByOrSeparator(); var badTypes = types.Where(type => !ModelInfoProvider.IsKnownResource(type)).ToHashSet(); if (badTypes.Count != 0) { _contextAccessor.RequestContext?.BundleIssues.Add( new OperationOutcomeIssue( OperationOutcomeConstants.IssueSeverity.Warning, OperationOutcomeConstants.IssueType.NotSupported, string.Format(Core.Resources.InvalidTypeParameter, badTypes.OrderBy(x => x).Select(type => $"'{type}'").JoinByOrSeparator()))); if (badTypes.Count != types.Count) { // In case of we have acceptable types, we filter invalid types from search. searchParams.Add(KnownQueryParameterNames.Type, types.Except(badTypes).JoinByOrSeparator()); } else { // If all types are invalid, we add them to search params. If we remove them, we wouldn't filter by type, and return all types, // which is incorrect behaviour. Optimally we should indicate in search options what it would yield nothing, and skip search, // but there is no option for that right now. searchParams.Add(KnownQueryParameterNames.Type, query.Item2); } } else { searchParams.Add(KnownQueryParameterNames.Type, query.Item2); } } else if (string.IsNullOrWhiteSpace(query.Item1) || string.IsNullOrWhiteSpace(query.Item2)) { // Query parameter with empty value is not supported. unsupportedSearchParameters.Add(query); } else if (string.Compare(query.Item1, KnownQueryParameterNames.Total, StringComparison.OrdinalIgnoreCase) == 0) { if (Enum.TryParse <TotalType>(query.Item2, true, out var totalType)) { ValidateTotalType(totalType); searchOptions.IncludeTotal = totalType; setDefaultBundleTotal = false; } else { throw new BadRequestException(string.Format(Core.Resources.InvalidTotalParameter, query.Item2, SupportedTotalTypes)); } } else { // Parse the search parameters. try { // Basic format checking (e.g. integer value for _count key etc.). searchParams.Add(query.Item1, query.Item2); } catch (Exception ex) { throw new BadRequestException(ex.Message); } } } searchOptions.ContinuationToken = continuationToken; if (setDefaultBundleTotal) { ValidateTotalType(_featureConfiguration.IncludeTotalInBundle); searchOptions.IncludeTotal = _featureConfiguration.IncludeTotalInBundle; } // Check the item count. if (searchParams.Count != null) { searchOptions.MaxItemCountSpecifiedByClient = true; if (searchParams.Count > _featureConfiguration.MaxItemCountPerSearch) { searchOptions.MaxItemCount = _featureConfiguration.MaxItemCountPerSearch; _contextAccessor.RequestContext?.BundleIssues.Add( new OperationOutcomeIssue( OperationOutcomeConstants.IssueSeverity.Information, OperationOutcomeConstants.IssueType.Informational, string.Format(Core.Resources.SearchParamaterCountExceedLimit, _featureConfiguration.MaxItemCountPerSearch, searchParams.Count))); } else { searchOptions.MaxItemCount = searchParams.Count.Value; } } else { searchOptions.MaxItemCount = _featureConfiguration.DefaultItemCountPerSearch; } searchOptions.IncludeCount = _featureConfiguration.DefaultIncludeCountPerSearch; if (searchParams.Elements?.Any() == true && searchParams.Summary != null && searchParams.Summary != SummaryType.False) { // The search parameters _elements and _summarize cannot be specified for the same request. throw new BadRequestException(string.Format(Core.Resources.ElementsAndSummaryParametersAreIncompatible, KnownQueryParameterNames.Summary, KnownQueryParameterNames.Elements)); } // Check to see if only the count should be returned searchOptions.CountOnly = searchParams.Summary == SummaryType.Count; // If the resource type is not specified, then the common // search parameters should be used. ResourceType[] parsedResourceTypes = new[] { ResourceType.DomainResource }; var searchExpressions = new List <Expression>(); if (string.IsNullOrWhiteSpace(resourceType)) { // Try to parse resource types from _type Search Parameter // This will result in empty array if _type has any modifiers // Which is good, since :not modifier changes the meaning of the // search parameter and we can no longer use it to deduce types // (and should proceed with ResourceType.DomainResource in that case) var resourceTypes = searchParams.Parameters .Where(q => q.Item1 == KnownQueryParameterNames.Type) // <-- Equality comparison to avoid modifiers .SelectMany(q => q.Item2.SplitByOrSeparator()) .Where(type => ModelInfoProvider.IsKnownResource(type)) .Select(x => { if (!Enum.TryParse(x, out ResourceType parsedType)) { // Should never get here throw new ResourceNotSupportedException(x); } return(parsedType); }) .Distinct(); if (resourceTypes.Any()) { parsedResourceTypes = resourceTypes.ToArray(); } } else { if (!Enum.TryParse(resourceType, out parsedResourceTypes[0])) { throw new ResourceNotSupportedException(resourceType); } searchExpressions.Add(Expression.SearchParameter(_resourceTypeSearchParameter, Expression.StringEquals(FieldName.TokenCode, null, resourceType, false))); } var resourceTypesString = parsedResourceTypes.Select(x => x.ToString()).ToArray(); searchExpressions.AddRange(searchParams.Parameters.Select( q => { try { return(_expressionParser.Parse(resourceTypesString, q.Item1, q.Item2)); } catch (SearchParameterNotSupportedException) { unsupportedSearchParameters.Add(q); return(null); } }) .Where(item => item != null)); if (searchParams.Include?.Count > 0) { searchExpressions.AddRange(searchParams.Include.Select( q => _expressionParser.ParseInclude(resourceTypesString, q, false /* not reversed */, false /* no iterate */)) .Where(item => item != null)); } if (searchParams.RevInclude?.Count > 0) { searchExpressions.AddRange(searchParams.RevInclude.Select( q => _expressionParser.ParseInclude(resourceTypesString, q, true /* reversed */, false /* no iterate */)) .Where(item => item != null)); } // Parse _include:iterate (_include:recurse) parameters. // :iterate (:recurse) modifiers are not supported by Hl7.Fhir.Rest, hence not added to the Include collection and exist in the Parameters list. // See https://github.com/FirelyTeam/fhir-net-api/issues/222 // _include:iterate (_include:recurse) expression may appear without a preceding _include parameter // when applied on a circular reference searchExpressions.AddRange(ParseIncludeIterateExpressions(searchParams)); // remove _include:iterate and _revinclude:iterate parameters from unsupportedSearchParameters unsupportedSearchParameters.RemoveAll(p => AllIterateModifiers.Contains(p.Item1)); if (!string.IsNullOrWhiteSpace(compartmentType)) { if (Enum.TryParse(compartmentType, out CompartmentType parsedCompartmentType)) { if (string.IsNullOrWhiteSpace(compartmentId)) { throw new InvalidSearchOperationException(Core.Resources.CompartmentIdIsInvalid); } searchExpressions.Add(Expression.CompartmentSearch(compartmentType, compartmentId)); } else { throw new InvalidSearchOperationException(string.Format(Core.Resources.CompartmentTypeIsInvalid, compartmentType)); } } if (searchExpressions.Count == 1) { searchOptions.Expression = searchExpressions[0]; } else if (searchExpressions.Count > 1) { searchOptions.Expression = Expression.And(searchExpressions.ToArray()); } if (unsupportedSearchParameters.Any()) { bool throwForUnsupported = false; if (_contextAccessor.RequestContext?.RequestHeaders != null && _contextAccessor.RequestContext.RequestHeaders.TryGetValue(KnownHeaders.Prefer, out var values)) { var handlingValue = values.FirstOrDefault(x => x.StartsWith("handling=", StringComparison.OrdinalIgnoreCase)); if (handlingValue != default) { handlingValue = handlingValue.Substring("handling=".Length); if (string.IsNullOrWhiteSpace(handlingValue) || !Enum.TryParse <SearchParameterHandling>(handlingValue, true, out var handling)) { throw new BadRequestException(string.Format( Core.Resources.InvalidHandlingValue, handlingValue, string.Join(",", Enum.GetNames <SearchParameterHandling>()))); } if (handling == SearchParameterHandling.Strict) { throwForUnsupported = true; } } } if (throwForUnsupported) { throw new BadRequestException(string.Format( Core.Resources.SearchParameterNotSupported, string.Join(",", unsupportedSearchParameters.Select(x => x.Item1)), string.Join(",", resourceTypesString))); } else { foreach (var unsupported in unsupportedSearchParameters) { _contextAccessor.RequestContext?.BundleIssues.Add(new OperationOutcomeIssue( OperationOutcomeConstants.IssueSeverity.Warning, OperationOutcomeConstants.IssueType.NotSupported, string.Format(CultureInfo.InvariantCulture, Core.Resources.SearchParameterNotSupported, unsupported.Item1, string.Join(",", resourceTypesString)))); } } } searchOptions.UnsupportedSearchParams = unsupportedSearchParameters; if (searchParams.Sort?.Count > 0) { var sortings = new List <(SearchParameterInfo, SortOrder)>(searchParams.Sort.Count); bool sortingsValid = true; foreach (Tuple <string, Hl7.Fhir.Rest.SortOrder> sorting in searchParams.Sort) { try { SearchParameterInfo searchParameterInfo = resourceTypesString.Select(t => _searchParameterDefinitionManager.GetSearchParameter(t, sorting.Item1)).Distinct().First(); sortings.Add((searchParameterInfo, sorting.Item2.ToCoreSortOrder())); } catch (SearchParameterNotSupportedException) { sortingsValid = false; _contextAccessor.RequestContext?.BundleIssues.Add(new OperationOutcomeIssue( OperationOutcomeConstants.IssueSeverity.Warning, OperationOutcomeConstants.IssueType.NotSupported, string.Format(CultureInfo.InvariantCulture, Core.Resources.SearchParameterNotSupported, sorting.Item1, string.Join(", ", resourceTypesString)))); } } if (sortingsValid) { if (!_sortingValidator.ValidateSorting(sortings, out IReadOnlyList <string> errorMessages)) { if (errorMessages == null || errorMessages.Count == 0) { throw new InvalidOperationException($"Expected {_sortingValidator.GetType().Name} to return error messages when {nameof(_sortingValidator.ValidateSorting)} returns false"); } sortingsValid = false; foreach (var errorMessage in errorMessages) { _contextAccessor.RequestContext?.BundleIssues.Add(new OperationOutcomeIssue( OperationOutcomeConstants.IssueSeverity.Warning, OperationOutcomeConstants.IssueType.NotSupported, errorMessage)); } } } if (sortingsValid) { searchOptions.Sort = sortings; } } if (searchOptions.Sort == null) { searchOptions.Sort = Array.Empty <(SearchParameterInfo searchParameterInfo, SortOrder sortOrder)>(); } return(searchOptions); IEnumerable <IncludeExpression> ParseIncludeIterateExpressions(SearchParams searchParams) { return(searchParams.Parameters .Where(p => p != null && AllIterateModifiers.Where(m => string.Equals(p.Item1, m, StringComparison.OrdinalIgnoreCase)).Any()) .Select(p => { var includeResourceType = p.Item2?.Split(':')[0]; if (!ModelInfoProvider.IsKnownResource(includeResourceType)) { throw new ResourceNotSupportedException(includeResourceType); } var reversed = RevIncludeIterateModifiers.Contains(p.Item1); var expression = _expressionParser.ParseInclude(new[] { includeResourceType }, p.Item2, reversed, true); // Reversed Iterate expressions (not wildcard) must specify target type if there is more than one possible target type if (expression.Reversed && expression.Iterate && expression.TargetResourceType == null && expression.ReferenceSearchParameter?.TargetResourceTypes?.Count > 1) { throw new BadRequestException(string.Format(Core.Resources.RevIncludeIterateTargetTypeNotSpecified, p.Item2)); } // For circular include iterate expressions, add an informational issue indicating that a single iteration is supported. // See https://www.hl7.org/fhir/search.html#revinclude. if (expression.Iterate && expression.CircularReference) { _contextAccessor.RequestContext?.BundleIssues.Add( new OperationOutcomeIssue( OperationOutcomeConstants.IssueSeverity.Information, OperationOutcomeConstants.IssueType.Informational, string.Format(Core.Resources.IncludeIterateCircularReferenceExecutedOnce, p.Item1, p.Item2))); } return expression; })); } void ValidateTotalType(TotalType totalType) { // Estimate is not yet supported. if (totalType == TotalType.Estimate) { throw new SearchOperationNotSupportedException(string.Format(Core.Resources.UnsupportedTotalParameter, totalType, SupportedTotalTypes)); } } }
public SearchOptions Create(string compartmentType, string compartmentId, string resourceType, IReadOnlyList <Tuple <string, string> > queryParameters) { var searchOptions = new SearchOptions(); string continuationToken = null; var searchParams = new SearchParams(); var unsupportedSearchParameters = new List <Tuple <string, string> >(); // Extract the continuation token, filter out the other known query parameters that's not search related. foreach (Tuple <string, string> query in queryParameters ?? Enumerable.Empty <Tuple <string, string> >()) { if (query.Item1 == KnownQueryParameterNames.ContinuationToken) { // This is an unreachable case. The mapping of the query parameters makes it so only one continuation token can exist. if (continuationToken != null) { throw new InvalidSearchOperationException( string.Format(Core.Resources.MultipleQueryParametersNotAllowed, KnownQueryParameterNames.ContinuationToken)); } // Checks if the continuation token is base 64 bit encoded. Needed for systems that have cached continuation tokens from before they were encoded. if (Base64FormatRegex.IsMatch(query.Item2)) { continuationToken = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(query.Item2)); } else { continuationToken = query.Item2; } } else if (query.Item1 == KnownQueryParameterNames.Format) { // TODO: We need to handle format parameter. } else if (string.IsNullOrWhiteSpace(query.Item1) || string.IsNullOrWhiteSpace(query.Item2)) { // Query parameter with empty value is not supported. unsupportedSearchParameters.Add(query); } else { // Parse the search parameters. try { searchParams.Add(query.Item1, query.Item2); } catch (Exception ex) { _logger.LogInformation(ex, "Failed to parse the query parameter. Skipping."); // There was a problem parsing the parameter. Add it to list of unsupported parameters. unsupportedSearchParameters.Add(query); } } } searchOptions.ContinuationToken = continuationToken; // Check the item count. if (searchParams.Count != null) { searchOptions.MaxItemCount = searchParams.Count.Value; } // Check to see if only the count should be returned searchOptions.CountOnly = searchParams.Summary == SummaryType.Count; // If the resource type is not specified, then the common // search parameters should be used. ResourceType parsedResourceType = ResourceType.DomainResource; if (!string.IsNullOrWhiteSpace(resourceType) && !Enum.TryParse(resourceType, out parsedResourceType)) { throw new ResourceNotSupportedException(resourceType); } var searchExpressions = new List <Expression>(); if (!string.IsNullOrWhiteSpace(resourceType)) { searchExpressions.Add(Expression.SearchParameter(_resourceTypeSearchParameter, Expression.StringEquals(FieldName.TokenCode, null, resourceType, false))); } searchExpressions.AddRange(searchParams.Parameters.Select( q => { try { return(_expressionParser.Parse(parsedResourceType.ToString(), q.Item1, q.Item2)); } catch (SearchParameterNotSupportedException) { unsupportedSearchParameters.Add(q); return(null); } }) .Where(item => item != null)); if (!string.IsNullOrWhiteSpace(compartmentType)) { if (Enum.TryParse(compartmentType, out CompartmentType parsedCompartmentType)) { if (string.IsNullOrWhiteSpace(compartmentId)) { throw new InvalidSearchOperationException(Core.Resources.CompartmentIdIsInvalid); } searchExpressions.Add(Expression.CompartmentSearch(compartmentType, compartmentId)); } else { throw new InvalidSearchOperationException(string.Format(Core.Resources.CompartmentTypeIsInvalid, compartmentType)); } } if (searchExpressions.Count == 1) { searchOptions.Expression = searchExpressions[0]; } else if (searchExpressions.Count > 1) { searchOptions.Expression = Expression.And(searchExpressions.ToArray()); } if (unsupportedSearchParameters.Any()) { // TODO: Client can specify whether exception should be raised or not when it encounters unknown search parameters. // For now, we will ignore any unknown search parameters. } searchOptions.UnsupportedSearchParams = unsupportedSearchParameters; if (searchParams.Sort?.Count > 0) { var sortings = new List <(SearchParameterInfo, SortOrder)>(); List <(string parameterName, string reason)> unsupportedSortings = null; foreach (Tuple <string, Hl7.Fhir.Rest.SortOrder> sorting in searchParams.Sort) { try { SearchParameterInfo searchParameterInfo = _searchParameterDefinitionManager.GetSearchParameter(parsedResourceType.ToString(), sorting.Item1); sortings.Add((searchParameterInfo, sorting.Item2.ToCoreSortOrder())); } catch (SearchParameterNotSupportedException) { (unsupportedSortings ?? (unsupportedSortings = new List <(string parameterName, string reason)>())).Add((sorting.Item1, string.Format(Core.Resources.SearchParameterNotSupported, sorting.Item1, resourceType))); } } searchOptions.Sort = sortings; searchOptions.UnsupportedSortingParams = (IReadOnlyList <(string parameterName, string reason)>)unsupportedSortings ?? Array.Empty <(string parameterName, string reason)>(); } else { searchOptions.Sort = Array.Empty <(SearchParameterInfo searchParameterInfo, SortOrder sortOrder)>(); searchOptions.UnsupportedSortingParams = Array.Empty <(string parameterName, string reason)>(); } return(searchOptions); }
public SearchOptions Create(string compartmentType, string compartmentId, string resourceType, IReadOnlyList <Tuple <string, string> > queryParameters) { var searchOptions = new SearchOptions(); string continuationToken = null; var searchParams = new SearchParams(); var unsupportedSearchParameters = new List <Tuple <string, string> >(); bool setDefaultBundleTotal = true; // Extract the continuation token, filter out the other known query parameters that's not search related. foreach (Tuple <string, string> query in queryParameters ?? Enumerable.Empty <Tuple <string, string> >()) { if (query.Item1 == KnownQueryParameterNames.ContinuationToken) { // This is an unreachable case. The mapping of the query parameters makes it so only one continuation token can exist. if (continuationToken != null) { throw new InvalidSearchOperationException( string.Format(Core.Resources.MultipleQueryParametersNotAllowed, KnownQueryParameterNames.ContinuationToken)); } // Checks if the continuation token is base 64 bit encoded. Needed for systems that have cached continuation tokens from before they were encoded. if (Base64FormatRegex.IsMatch(query.Item2)) { continuationToken = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(query.Item2)); } else { continuationToken = query.Item2; } setDefaultBundleTotal = false; } else if (query.Item1 == KnownQueryParameterNames.Format) { // TODO: We need to handle format parameter. } else if (string.IsNullOrWhiteSpace(query.Item1) || string.IsNullOrWhiteSpace(query.Item2)) { // Query parameter with empty value is not supported. unsupportedSearchParameters.Add(query); } else if (string.Compare(query.Item1, KnownQueryParameterNames.Total, StringComparison.OrdinalIgnoreCase) == 0) { if (Enum.TryParse <TotalType>(query.Item2, true, out var totalType)) { ValidateTotalType(totalType); searchOptions.IncludeTotal = totalType; setDefaultBundleTotal = false; } else { throw new BadRequestException(string.Format(Core.Resources.InvalidTotalParameter, query.Item2, SupportedTotalTypes)); } } else { // Parse the search parameters. try { // Basic format checking (e.g. integer value for _count key etc.). searchParams.Add(query.Item1, query.Item2); } catch (Exception ex) { throw new BadRequestException(ex.Message); } } } searchOptions.ContinuationToken = continuationToken; if (setDefaultBundleTotal) { ValidateTotalType(_featureConfiguration.IncludeTotalInBundle); searchOptions.IncludeTotal = _featureConfiguration.IncludeTotalInBundle; } // Check the item count. if (searchParams.Count != null) { searchOptions.MaxItemCount = searchParams.Count.Value; } // Check to see if only the count should be returned searchOptions.CountOnly = searchParams.Summary == SummaryType.Count; // If the resource type is not specified, then the common // search parameters should be used. ResourceType parsedResourceType = ResourceType.DomainResource; if (!string.IsNullOrWhiteSpace(resourceType) && !Enum.TryParse(resourceType, out parsedResourceType)) { throw new ResourceNotSupportedException(resourceType); } var searchExpressions = new List <Expression>(); if (!string.IsNullOrWhiteSpace(resourceType)) { searchExpressions.Add(Expression.SearchParameter(_resourceTypeSearchParameter, Expression.StringEquals(FieldName.TokenCode, null, resourceType, false))); } searchExpressions.AddRange(searchParams.Parameters.Select( q => { try { return(_expressionParser.Parse(parsedResourceType.ToString(), q.Item1, q.Item2)); } catch (SearchParameterNotSupportedException) { unsupportedSearchParameters.Add(q); return(null); } }) .Where(item => item != null)); if (searchParams.Include?.Count > 0) { searchExpressions.AddRange(searchParams.Include.Select( q => _expressionParser.ParseInclude(parsedResourceType.ToString(), q, false /* not reversed */)) .Where(item => item != null)); } if (searchParams.RevInclude?.Count > 0) { searchExpressions.AddRange(searchParams.RevInclude.Select( q => _expressionParser.ParseInclude(parsedResourceType.ToString(), q, true /* reversed */)) .Where(item => item != null)); } if (!string.IsNullOrWhiteSpace(compartmentType)) { if (Enum.TryParse(compartmentType, out CompartmentType parsedCompartmentType)) { if (string.IsNullOrWhiteSpace(compartmentId)) { throw new InvalidSearchOperationException(Core.Resources.CompartmentIdIsInvalid); } searchExpressions.Add(Expression.CompartmentSearch(compartmentType, compartmentId)); } else { throw new InvalidSearchOperationException(string.Format(Core.Resources.CompartmentTypeIsInvalid, compartmentType)); } } if (searchExpressions.Count == 1) { searchOptions.Expression = searchExpressions[0]; } else if (searchExpressions.Count > 1) { searchOptions.Expression = Expression.And(searchExpressions.ToArray()); } if (unsupportedSearchParameters.Any()) { // TODO: Client can specify whether exception should be raised or not when it encounters unknown search parameters. // For now, we will ignore any unknown search parameters. } searchOptions.UnsupportedSearchParams = unsupportedSearchParameters; if (searchParams.Sort?.Count > 0) { var sortings = new List <(SearchParameterInfo, SortOrder)>(); List <(string parameterName, string reason)> unsupportedSortings = null; foreach (Tuple <string, Hl7.Fhir.Rest.SortOrder> sorting in searchParams.Sort) { try { SearchParameterInfo searchParameterInfo = _searchParameterDefinitionManager.GetSearchParameter(parsedResourceType.ToString(), sorting.Item1); if (searchParameterInfo.IsSortSupported()) { sortings.Add((searchParameterInfo, sorting.Item2.ToCoreSortOrder())); } else { throw new SearchParameterNotSupportedException(string.Format(Core.Resources.SearchSortParameterNotSupported, searchParameterInfo.Name)); } } catch (SearchParameterNotSupportedException) { (unsupportedSortings ??= new List <(string parameterName, string reason)>()).Add((sorting.Item1, string.Format(Core.Resources.SearchSortParameterNotSupported, sorting.Item1))); } } searchOptions.Sort = sortings; searchOptions.UnsupportedSortingParams = (IReadOnlyList <(string parameterName, string reason)>)unsupportedSortings ?? Array.Empty <(string parameterName, string reason)>(); } else { searchOptions.Sort = Array.Empty <(SearchParameterInfo searchParameterInfo, SortOrder sortOrder)>(); searchOptions.UnsupportedSortingParams = Array.Empty <(string parameterName, string reason)>(); } return(searchOptions); void ValidateTotalType(TotalType totalType) { // Estimate is not yet supported. if (totalType == TotalType.Estimate) { throw new SearchOperationNotSupportedException(string.Format(Core.Resources.UnsupportedTotalParameter, totalType, SupportedTotalTypes)); } } }