public void GivenACompositeWithVariousTypes_WhenBuilt_ThenCorrectExpressionShouldBeCreated() { const string system = "system"; const string code = "code"; const decimal quantity = 10; const string quantitySystem = "quantity-system"; const string quantityCode = "quantity-code"; var codeUri = new Uri("http://code"); var quantityUri = new Uri("http://quantity"); SearchParameter searchParameter = CreateCompositeSearchParameter( new ComponentComponent() { Definition = new ResourceReference(codeUri.ToString()), }, new ComponentComponent() { Definition = new ResourceReference(quantityUri.ToString()), }); _searchParameterDefinitionManager.GetSearchParameter(codeUri).Returns( new SearchParameter() { Name = "code", Type = SearchParamType.Token, }); _searchParameterDefinitionManager.GetSearchParameter(quantityUri).Returns( new SearchParameter() { Name = "quantity", Type = SearchParamType.Quantity, }); Validate( searchParameter, null, $"{system}|{code}${quantity}|{quantitySystem}|{quantityCode}", outer => ValidateMultiaryExpression( outer, MultiaryOperator.And, e => ValidateMultiaryExpression( e, MultiaryOperator.And, se => ValidateStringExpression(se, FieldName.TokenSystem, StringOperator.Equals, system, false), se => ValidateStringExpression(se, FieldName.TokenCode, StringOperator.Equals, code, false)), e => ValidateMultiaryExpression( e, MultiaryOperator.And, e1 => ValidateStringExpression(e1, FieldName.QuantitySystem, StringOperator.Equals, quantitySystem, false), e1 => ValidateStringExpression(e1, FieldName.QuantityCode, StringOperator.Equals, quantityCode, false), e1 => ValidateMultiaryExpression( e1, MultiaryOperator.And, e2 => ValidateBinaryExpression(e2, FieldName.Quantity, BinaryOperator.GreaterThanOrEqual, 9.5m), e2 => ValidateBinaryExpression(e2, FieldName.Quantity, BinaryOperator.LessThanOrEqual, 10.5m))))); }
public IncludeExpression ParseInclude(string resourceType, string includeValue, bool isReversed, bool iterate) { var valueSpan = includeValue.AsSpan(); if (!TrySplit(SearchSplitChar, ref valueSpan, out ReadOnlySpan <char> originalType)) { throw new InvalidSearchOperationException(isReversed ? Core.Resources.RevIncludeMissingType : Core.Resources.IncludeMissingType); } if (resourceType.Equals(KnownResourceTypes.DomainResource, StringComparison.InvariantCultureIgnoreCase)) { throw new InvalidSearchOperationException(Core.Resources.IncludeCannotBeAgainstBase); } SearchParameterInfo refSearchParameter; List <string> referencedTypes = null; bool wildCard = false; string targetType = null; if (valueSpan.Equals("*".AsSpan(), StringComparison.InvariantCultureIgnoreCase)) { refSearchParameter = null; wildCard = true; } else { if (!TrySplit(SearchSplitChar, ref valueSpan, out ReadOnlySpan <char> searchParam)) { searchParam = valueSpan; } else { targetType = valueSpan.ToString(); } refSearchParameter = _searchParameterDefinitionManager.GetSearchParameter(originalType.ToString(), searchParam.ToString()); } if (wildCard) { referencedTypes = new List <string>(); var searchParameters = _searchParameterDefinitionManager.GetSearchParameters(resourceType) .Where(p => p.Type == ValueSets.SearchParamType.Reference); foreach (var p in searchParameters) { foreach (var t in p.TargetResourceTypes) { if (!referencedTypes.Contains(t)) { referencedTypes.Add(t); } } } } return(new IncludeExpression(resourceType, refSearchParameter, originalType.ToString(), targetType, referencedTypes, wildCard, isReversed, iterate)); }
public SearchParameterInfo GetSearchParameter(string resourceType, string name) { SearchParameterInfo parameter = _inner.GetSearchParameter(resourceType, name); if (parameter.IsSupported) { return(parameter); } throw new SearchParameterNotSupportedException(resourceType, name); }
public async Task UpdateSearchParameterStatusAsync(IReadOnlyCollection <string> searchParameterUris, SearchParameterStatus status) { var searchParameterStatusList = new List <ResourceSearchParameterStatus>(); var updated = new List <SearchParameterInfo>(); foreach (string uri in searchParameterUris) { var searchParamUri = new Uri(uri); var paramInfo = _searchParameterDefinitionManager.GetSearchParameter(searchParamUri); updated.Add(paramInfo); paramInfo.IsSearchable = status == SearchParameterStatus.Enabled; paramInfo.IsSupported = status == SearchParameterStatus.Supported || status == SearchParameterStatus.Enabled; searchParameterStatusList.Add(new ResourceSearchParameterStatus() { LastUpdated = Clock.UtcNow, Status = status, Uri = searchParamUri, }); } await _searchParameterStatusDataStore.UpsertStatuses(searchParameterStatusList); await _mediator.Publish(new SearchParametersUpdated(updated)); }
public SearchParameterValidatorTests() { _searchParameterDefinitionManager.When(s => s.GetSearchParameter(Arg.Is <Uri>(uri => uri != new Uri("http://duplicate")))). Do(x => throw new SearchParameterNotSupportedException("message")); _searchParameterDefinitionManager.GetSearchParameter(new Uri("http://duplicate")).Returns(new SearchParameterInfo("duplicate", "duplicate")); _fhirOperationDataStore.CheckActiveReindexJobsAsync(CancellationToken.None).Returns((false, string.Empty)); }
public async Task AddSearchParameterAsync(ITypedElement searchParam, CancellationToken cancellationToken) { try { // verify the parameter is supported before continuing var searchParameterWrapper = new SearchParameterWrapper(searchParam); var searchParameterInfo = new SearchParameterInfo(searchParameterWrapper); if (searchParameterInfo.Component?.Any() == true) { foreach (SearchParameterComponentInfo c in searchParameterInfo.Component) { c.ResolvedSearchParameter = _searchParameterDefinitionManager.GetSearchParameter(c.DefinitionUrl.OriginalString); } } (bool Supported, bool IsPartiallySupported)supportedResult = _searchParameterSupportResolver.IsSearchParameterSupported(searchParameterInfo); if (!supportedResult.Supported) { throw new SearchParameterNotSupportedException(searchParameterInfo.Url); } // check data store specific support for SearchParameter if (!_dataStoreSearchParameterValidator.ValidateSearchParameter(searchParameterInfo, out var errorMessage)) { throw new SearchParameterNotSupportedException(errorMessage); } _logger.LogTrace("Adding the search parameter '{url}'", searchParameterWrapper.Url); _searchParameterDefinitionManager.AddNewSearchParameters(new List <ITypedElement> { searchParam }); await _searchParameterStatusManager.AddSearchParameterStatusAsync(new List <string> { searchParameterWrapper.Url }, cancellationToken); } catch (FhirException fex) { fex.Issues.Add(new OperationOutcomeIssue( OperationOutcomeConstants.IssueSeverity.Error, OperationOutcomeConstants.IssueType.Exception, Core.Resources.CustomSearchCreateError)); throw; } catch (Exception ex) { var customSearchException = new ConfigureCustomSearchException(Core.Resources.CustomSearchCreateError); customSearchException.Issues.Add(new OperationOutcomeIssue( OperationOutcomeConstants.IssueSeverity.Error, OperationOutcomeConstants.IssueType.Exception, ex.Message)); throw customSearchException; } }
private IEnumerable <SearchIndexEntry> ProcessCompositeSearchParameter(SearchParameterInfo searchParameter, Base resource, FhirEvaluationContext context) { Debug.Assert(searchParameter?.Type == SearchParamType.Composite, "The search parameter must be composite."); SearchParameterInfo compositeSearchParameterInfo = searchParameter; IEnumerable <Base> rootObjects = resource.Select(searchParameter.Expression, context); foreach (var rootObject in rootObjects) { int numberOfComponents = searchParameter.Component.Count; bool skip = false; var componentValues = new IReadOnlyList <ISearchValue> [numberOfComponents]; // For each object extracted from the expression, we will need to evaluate each component. for (int i = 0; i < numberOfComponents; i++) { SearchParameterComponentInfo component = searchParameter.Component[i]; // First find the type of the component. SearchParameterInfo componentSearchParameterDefinition = _searchParameterDefinitionManager.GetSearchParameter(component.DefinitionUrl); IReadOnlyList <ISearchValue> extractedComponentValues = ExtractSearchValues( componentSearchParameterDefinition.Url.ToString(), componentSearchParameterDefinition.Type, componentSearchParameterDefinition.TargetResourceTypes, rootObject, component.Expression, context); // Filter out any search value that's not valid as a composite component. extractedComponentValues = extractedComponentValues .Where(sv => sv.IsValidAsCompositeComponent) .ToArray(); if (!extractedComponentValues.Any()) { // One of the components didn't have any value and therefore it will not be indexed. skip = true; break; } componentValues[i] = extractedComponentValues; } if (skip) { continue; } yield return(new SearchIndexEntry(compositeSearchParameterInfo, new CompositeSearchValue(componentValues))); } }
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 IncludeExpression ParseInclude(string resourceType, string includeValue) { var valueSpan = includeValue.AsSpan(); if (!TrySplit(SearchSplitChar, ref valueSpan, out ReadOnlySpan <char> originalType)) { throw new InvalidSearchOperationException(Core.Resources.IncludeMissingType); } if (resourceType.Equals(typeof(DomainResource).Name, StringComparison.InvariantCultureIgnoreCase)) { throw new InvalidSearchOperationException(Core.Resources.IncludeCannotBeAgainstBase); } SearchParameterInfo refSearchParameter; bool wildCard = false; string targetType = null; if (valueSpan.Equals("*".AsSpan(), StringComparison.InvariantCultureIgnoreCase)) { refSearchParameter = null; wildCard = true; } else { if (!TrySplit(SearchSplitChar, ref valueSpan, out ReadOnlySpan <char> searchParam)) { searchParam = valueSpan; } else { targetType = valueSpan.ToString(); } refSearchParameter = _searchParameterDefinitionManager.GetSearchParameter(originalType.ToString(), searchParam.ToString()); } return(new IncludeExpression(resourceType, refSearchParameter, targetType, wildCard)); }
public SearchOptionsFactory( IExpressionParser expressionParser, ISearchParameterDefinitionManager searchParameterDefinitionManager, ILogger <SearchOptionsFactory> logger) { EnsureArg.IsNotNull(expressionParser, nameof(expressionParser)); EnsureArg.IsNotNull(searchParameterDefinitionManager, nameof(searchParameterDefinitionManager)); EnsureArg.IsNotNull(logger, nameof(logger)); _expressionParser = expressionParser; _logger = logger; _resourceTypeSearchParameter = searchParameterDefinitionManager.GetSearchParameter(ResourceType.Resource.ToString(), SearchParameterNames.ResourceType); }
public SearchOptionsFactory( IExpressionParser expressionParser, ISearchParameterDefinitionManager.SearchableSearchParameterDefinitionManagerResolver searchParameterDefinitionManagerResolver, IOptions <CoreFeatureConfiguration> featureConfiguration, ILogger <SearchOptionsFactory> logger) { EnsureArg.IsNotNull(expressionParser, nameof(expressionParser)); EnsureArg.IsNotNull(searchParameterDefinitionManagerResolver, nameof(searchParameterDefinitionManagerResolver)); EnsureArg.IsNotNull(featureConfiguration?.Value, nameof(featureConfiguration)); EnsureArg.IsNotNull(logger, nameof(logger)); _expressionParser = expressionParser; _searchParameterDefinitionManager = searchParameterDefinitionManagerResolver(); _logger = logger; _featureConfiguration = featureConfiguration.Value; _resourceTypeSearchParameter = _searchParameterDefinitionManager.GetSearchParameter(ResourceType.Resource.ToString(), SearchParameterNames.ResourceType); }
public FhirCosmosSearchService( ISearchOptionsFactory searchOptionsFactory, CosmosFhirDataStore fhirDataStore, IQueryBuilder queryBuilder, ISearchParameterDefinitionManager searchParameterDefinitionManager, IFhirRequestContextAccessor requestContextAccessor) : base(searchOptionsFactory, fhirDataStore) { EnsureArg.IsNotNull(fhirDataStore, nameof(fhirDataStore)); EnsureArg.IsNotNull(queryBuilder, nameof(queryBuilder)); EnsureArg.IsNotNull(searchParameterDefinitionManager, nameof(searchParameterDefinitionManager)); EnsureArg.IsNotNull(requestContextAccessor, nameof(requestContextAccessor)); _fhirDataStore = fhirDataStore; _queryBuilder = queryBuilder; _requestContextAccessor = requestContextAccessor; _resourceTypeSearchParameter = searchParameterDefinitionManager.GetSearchParameter(KnownResourceTypes.Resource, SearchParameterNames.ResourceType); }
public async Task UpdateSearchParameterStatusAsync(IReadOnlyCollection <string> searchParameterUris, SearchParameterStatus status) { var searchParameterStatusList = new List <ResourceSearchParameterStatus>(); var updated = new List <SearchParameterInfo>(); var parameters = (await _searchParameterStatusDataStore.GetSearchParameterStatuses()) .ToDictionary(x => x.Uri); foreach (string uri in searchParameterUris) { var searchParamUri = new Uri(uri); var paramInfo = _searchParameterDefinitionManager.GetSearchParameter(searchParamUri); updated.Add(paramInfo); paramInfo.IsSearchable = status == SearchParameterStatus.Enabled; paramInfo.IsSupported = status == SearchParameterStatus.Supported || status == SearchParameterStatus.Enabled; if (parameters.TryGetValue(searchParamUri, out var existingStatus)) { existingStatus.LastUpdated = Clock.UtcNow; existingStatus.Status = status; if (paramInfo.IsSearchable && existingStatus.SortStatus == SortParameterStatus.Supported) { existingStatus.SortStatus = SortParameterStatus.Enabled; paramInfo.SortStatus = SortParameterStatus.Enabled; } searchParameterStatusList.Add(existingStatus); } else { searchParameterStatusList.Add(new ResourceSearchParameterStatus { LastUpdated = Clock.UtcNow, Status = status, Uri = searchParamUri, }); } } await _searchParameterStatusDataStore.UpsertStatuses(searchParameterStatusList); await _mediator.Publish(new SearchParametersUpdated(updated)); }
public void GivenACompositeWithInvalidModifier_WhenBuilding_ThenInvalidSearchOperationExceptionShouldBeThrown(SearchModifier modifier) { var quantityUri = new Uri("http://quantity"); SearchParameterComponentInfo[] components = new[] { new SearchParameterComponentInfo(quantityUri), new SearchParameterComponentInfo(quantityUri) }; var searchParameter1 = new SearchParameterInfo( DefaultParamName, Microsoft.Health.Fhir.ValueSets.SearchParamType.Composite, components: components); SearchParameterInfo searchParameter = searchParameter1; _searchParameterDefinitionManager.GetSearchParameter(quantityUri).Returns( new SearchParameter { Name = "quantity", Type = SearchParamType.Quantity, }.ToInfo()); Assert.Throws <InvalidSearchOperationException>( () => _parser.Parse(CreateSearchParameter(SearchParamType.Composite), modifier, "10|s|c$10|s|c")); }
public async Task <IReadOnlyCollection <ResourceSearchParameterStatus> > GetSearchParameterStatuses(CancellationToken cancellationToken) { // If the search parameter table in SQL does not yet contain status columns if (_schemaInformation.Current < SchemaVersionConstants.SearchParameterStatusSchemaVersion) { // Get status information from file. return(await _filebasedSearchParameterStatusDataStore.GetSearchParameterStatuses(cancellationToken)); } using (IScoped <SqlConnectionWrapperFactory> scopedSqlConnectionWrapperFactory = _scopedSqlConnectionWrapperFactory()) using (SqlConnectionWrapper sqlConnectionWrapper = await scopedSqlConnectionWrapperFactory.Value.ObtainSqlConnectionWrapperAsync(cancellationToken, true)) using (SqlCommandWrapper sqlCommandWrapper = sqlConnectionWrapper.CreateSqlCommand()) { VLatest.GetSearchParamStatuses.PopulateCommand(sqlCommandWrapper); var parameterStatuses = new List <ResourceSearchParameterStatus>(); using (SqlDataReader sqlDataReader = await sqlCommandWrapper.ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken)) { while (await sqlDataReader.ReadAsync(cancellationToken)) { short id; string uri; string stringStatus; DateTimeOffset?lastUpdated; bool? isPartiallySupported; ResourceSearchParameterStatus resourceSearchParameterStatus; if (_schemaInformation.Current >= SchemaVersionConstants.SearchParameterSynchronizationVersion) { (id, uri, stringStatus, lastUpdated, isPartiallySupported) = sqlDataReader.ReadRow( VLatest.SearchParam.SearchParamId, VLatest.SearchParam.Uri, VLatest.SearchParam.Status, VLatest.SearchParam.LastUpdated, VLatest.SearchParam.IsPartiallySupported); if (string.IsNullOrEmpty(stringStatus) || lastUpdated == null || isPartiallySupported == null) { // These columns are nullable because they are added to dbo.SearchParam in a later schema version. // They should be populated as soon as they are added to the table and should never be null. throw new SearchParameterNotSupportedException(Resources.SearchParameterStatusShouldNotBeNull); } var status = Enum.Parse <SearchParameterStatus>(stringStatus, true); resourceSearchParameterStatus = new SqlServerResourceSearchParameterStatus { Id = id, Uri = new Uri(uri), Status = status, IsPartiallySupported = (bool)isPartiallySupported, LastUpdated = (DateTimeOffset)lastUpdated, }; } else { (uri, stringStatus, lastUpdated, isPartiallySupported) = sqlDataReader.ReadRow( VLatest.SearchParam.Uri, VLatest.SearchParam.Status, VLatest.SearchParam.LastUpdated, VLatest.SearchParam.IsPartiallySupported); if (string.IsNullOrEmpty(stringStatus) || lastUpdated == null || isPartiallySupported == null) { // These columns are nullable because they are added to dbo.SearchParam in a later schema version. // They should be populated as soon as they are added to the table and should never be null. throw new SearchParameterNotSupportedException(Resources.SearchParameterStatusShouldNotBeNull); } var status = Enum.Parse <SearchParameterStatus>(stringStatus, true); resourceSearchParameterStatus = new ResourceSearchParameterStatus { Uri = new Uri(uri), Status = status, IsPartiallySupported = (bool)isPartiallySupported, LastUpdated = (DateTimeOffset)lastUpdated, }; } if (_schemaInformation.Current >= SchemaVersionConstants.AddMinMaxForDateAndStringSearchParamVersion) { // For schema versions starting from AddMinMaxForDateAndStringSearchParamVersion we will check // whether the corresponding type of the search parameter is supported. SearchParameterInfo paramInfo = null; try { paramInfo = _searchParameterDefinitionManager.GetSearchParameter(resourceSearchParameterStatus.Uri.OriginalString); } catch (SearchParameterNotSupportedException) { } if (paramInfo != null && SqlServerSortingValidator.SupportedSortParamTypes.Contains(paramInfo.Type)) { resourceSearchParameterStatus.SortStatus = SortParameterStatus.Enabled; } else { resourceSearchParameterStatus.SortStatus = SortParameterStatus.Disabled; } } else { if (_sortingValidator.SupportedParameterUris.Contains(resourceSearchParameterStatus.Uri)) { resourceSearchParameterStatus.SortStatus = SortParameterStatus.Enabled; } else { resourceSearchParameterStatus.SortStatus = SortParameterStatus.Disabled; } } parameterStatuses.Add(resourceSearchParameterStatus); } } return(parameterStatuses); } }
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)); } } }
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 SearchParameterStatusManagerTests() { _searchParameterRegistry = Substitute.For <ISearchParameterRegistry>(); _searchParameterDefinitionManager = Substitute.For <ISearchParameterDefinitionManager>(); _searchParameterSupportResolver = Substitute.For <ISearchParameterSupportResolver>(); _mediator = Substitute.For <IMediator>(); _manager = new SearchParameterStatusManager( _searchParameterRegistry, _searchParameterDefinitionManager, _searchParameterSupportResolver, _mediator); _searchParameterRegistry.GetSearchParameterStatuses() .Returns(new[] { new ResourceSearchParameterStatus { Status = SearchParameterStatus.Enabled, Uri = new Uri(ResourceId), }, new ResourceSearchParameterStatus { Status = SearchParameterStatus.Enabled, Uri = new Uri(ResourceLastupdated), IsPartiallySupported = true, }, new ResourceSearchParameterStatus { Status = SearchParameterStatus.Disabled, Uri = new Uri(ResourceProfile), }, new ResourceSearchParameterStatus { Status = SearchParameterStatus.Supported, Uri = new Uri(ResourceSecurity), }, }); _queryParameter = new SearchParameterInfo("_query", SearchParamType.Token, new Uri(ResourceQuery)); _searchParameterInfos = new[] { new SearchParameterInfo("_id", SearchParamType.Token, new Uri(ResourceId)), new SearchParameterInfo("_lastUpdated", SearchParamType.Token, new Uri(ResourceLastupdated)), new SearchParameterInfo("_profile", SearchParamType.Token, new Uri(ResourceProfile)), new SearchParameterInfo("_security", SearchParamType.Token, new Uri(ResourceSecurity)), _queryParameter, }; _searchParameterDefinitionManager.GetSearchParameters("Account") .Returns(_searchParameterInfos); _searchParameterDefinitionManager.AllSearchParameters .Returns(_searchParameterInfos); _searchParameterDefinitionManager.GetSearchParameter(new Uri(ResourceQuery)) .Returns(_queryParameter); _searchParameterSupportResolver .IsSearchParameterSupported(Arg.Any <SearchParameterInfo>()) .Returns((false, false)); _searchParameterSupportResolver .IsSearchParameterSupported(Arg.Is(_searchParameterInfos[4])) .Returns((true, false)); }
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 async Task ValidateSearchParamterInput(SearchParameter searchParam, string method, CancellationToken cancellationToken) { if (await _authorizationService.CheckAccess(DataActions.Reindex) != DataActions.Reindex) { throw new UnauthorizedFhirActionException(); } // check if reindex job is running using (IScoped <IFhirOperationDataStore> fhirOperationDataStore = _fhirOperationDataStoreFactory()) { (var activeReindexJobs, var reindexJobId) = await fhirOperationDataStore.Value.CheckActiveReindexJobsAsync(cancellationToken); if (activeReindexJobs) { throw new JobConflictException(string.Format(Resources.ChangesToSearchParametersNotAllowedWhileReindexing, reindexJobId)); } } var validationFailures = new List <ValidationFailure>(); if (string.IsNullOrEmpty(searchParam.Url)) { validationFailures.Add( new ValidationFailure(nameof(Base.TypeName), Resources.SearchParameterDefinitionInvalidMissingUri)); } else { try { _searchParameterDefinitionManager.GetSearchParameter(new Uri(searchParam.Url)); // If a post, then it is a creation of a new search parameter // only allow this if no other parameters exist with the same Uri if (method.Equals(HttpPostName, StringComparison.OrdinalIgnoreCase)) { // if no exception is thrown, then the search parameter with the same Uri was found // and this is a conflict validationFailures.Add( new ValidationFailure( nameof(searchParam.Url), string.Format(Resources.SearchParameterDefinitionDuplicatedEntry, searchParam.Url))); } } catch (FormatException) { validationFailures.Add( new ValidationFailure( nameof(searchParam.Url), string.Format(Resources.SearchParameterDefinitionInvalidDefinitionUri, searchParam.Url))); } catch (SearchParameterNotSupportedException) { // if thrown, then the search parameter is not found // if a PUT, then we should be updating an exsting paramter, but it was not found if (method.Equals(HttpPutName, StringComparison.OrdinalIgnoreCase) || method.Equals(HttpDeleteName, StringComparison.OrdinalIgnoreCase)) { // if an exception above was thrown, then the search parameter with the same Uri was not found // and DELETE or PUT can only run on existing parameter validationFailures.Add( new ValidationFailure( nameof(searchParam.Url), string.Format(Resources.SearchParameterDefinitionNotFound, searchParam.Url))); } } } // validate that the url does not correspond to a search param from the spec // TODO: still need a method to determine spec defined search params // validation of the fhir path // TODO: separate user story for this validation if (validationFailures.Any()) { throw new ResourceNotValidException(validationFailures); } }
public Expression Parse( SearchParameterInfo searchParameter, SearchModifierCode?modifier, string value) { EnsureArg.IsNotNull(searchParameter, nameof(searchParameter)); Debug.Assert( modifier == null || Enum.IsDefined(typeof(SearchModifierCode), modifier.Value), "Invalid modifier."); EnsureArg.IsNotNullOrWhiteSpace(value, nameof(value)); Expression outputExpression; if (modifier == SearchModifierCode.Missing) { // We have to handle :missing modifier specially because if :missing modifier is specified, // then the value is a boolean string indicating whether the parameter is missing or not instead of // the search value type associated with the search parameter. if (!bool.TryParse(value, out bool isMissing)) { // An invalid value was specified. throw new InvalidSearchOperationException(Core.Resources.InvalidValueTypeForMissingModifier); } return(Expression.MissingSearchParameter(searchParameter, isMissing)); } if (modifier == SearchModifierCode.Text) { // We have to handle :text modifier specially because if :text modifier is supplied for token search param, // then we want to search the display text using the specified text, and therefore // we don't want to actually parse the specified text into token. if (searchParameter.Type != ValueSets.SearchParamType.Token) { throw new InvalidSearchOperationException( string.Format(CultureInfo.InvariantCulture, Core.Resources.ModifierNotSupported, modifier, searchParameter.Name)); } outputExpression = Expression.Contains(FieldName.TokenText, null, value, true); } else { // Build the expression for based on the search value. if (searchParameter.Type == ValueSets.SearchParamType.Composite) { if (modifier != null) { throw new InvalidSearchOperationException( string.Format(CultureInfo.InvariantCulture, Core.Resources.ModifierNotSupported, modifier, searchParameter.Name)); } IReadOnlyList <string> compositeValueParts = value.SplitByCompositeSeparator(); if (compositeValueParts.Count > searchParameter.Component.Count) { throw new InvalidSearchOperationException( string.Format(CultureInfo.InvariantCulture, Core.Resources.NumberOfCompositeComponentsExceeded, searchParameter.Name)); } var compositeExpressions = new Expression[compositeValueParts.Count]; var searchParameterComponentInfos = searchParameter.Component.ToList(); for (int i = 0; i < compositeValueParts.Count; i++) { var component = searchParameterComponentInfos[i]; // Find the corresponding search parameter info. SearchParameterInfo componentSearchParameter = _searchParameterDefinitionManager.GetSearchParameter(component.DefinitionUrl); string componentValue = compositeValueParts[i]; compositeExpressions[i] = Build( componentSearchParameter, modifier: null, componentIndex: i, value: componentValue); } outputExpression = Expression.And(compositeExpressions); } else { outputExpression = Build( searchParameter, modifier, componentIndex: null, value: value); } } return(Expression.SearchParameter(searchParameter, outputExpression)); }
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); }
private Type GetSearchValueTypeImpl(SearchParameterInfo searchParameter) { switch (searchParameter.Type) { case SearchParamType.Number: return(typeof(NumberSearchValue)); case SearchParamType.Date: return(typeof(DateTimeSearchValue)); case SearchParamType.String: return(typeof(StringSearchValue)); case SearchParamType.Token: return(typeof(TokenSearchValue)); case SearchParamType.Reference: return(typeof(ReferenceSearchValue)); case SearchParamType.Quantity: return(typeof(QuantitySearchValue)); case SearchParamType.Uri: return(typeof(UriSearchValue)); case SearchParamType.Composite: return(typeof(Tuple).Assembly.GetType($"{typeof(ValueTuple).FullName}`{searchParameter.Component.Count}", throwOnError: true) .MakeGenericType(searchParameter.Component.Select(c => GetSearchValueType(_searchParameterDefinitionManager.GetSearchParameter(c.DefinitionUrl))).ToArray())); default: throw new ArgumentOutOfRangeException(searchParameter.Code); } }
public SearchParameterStatusManagerTests() { _searchParameterStatusDataStore = Substitute.For <ISearchParameterStatusDataStore>(); _searchParameterDefinitionManager = Substitute.For <ISearchParameterDefinitionManager>(); _searchParameterSupportResolver = Substitute.For <ISearchParameterSupportResolver>(); _mediator = Substitute.For <IMediator>(); _manager = new SearchParameterStatusManager( _searchParameterStatusDataStore, _searchParameterDefinitionManager, _searchParameterSupportResolver, _mediator); _resourceSearchParameterStatuses = new[] { new ResourceSearchParameterStatus { Status = SearchParameterStatus.Enabled, Uri = new Uri(ResourceId), LastUpdated = Clock.UtcNow, }, new ResourceSearchParameterStatus { Status = SearchParameterStatus.Enabled, Uri = new Uri(ResourceLastupdated), IsPartiallySupported = true, LastUpdated = Clock.UtcNow, }, new ResourceSearchParameterStatus { Status = SearchParameterStatus.Disabled, Uri = new Uri(ResourceProfile), LastUpdated = Clock.UtcNow, }, new ResourceSearchParameterStatus { Status = SearchParameterStatus.Supported, Uri = new Uri(ResourceSecurity), LastUpdated = Clock.UtcNow, }, }; _searchParameterStatusDataStore.GetSearchParameterStatuses().Returns(_resourceSearchParameterStatuses); List <string> baseResourceTypes = new List <string>() { "Resource" }; List <string> targetResourceTypes = new List <string>() { "Patient" }; _queryParameter = new SearchParameterInfo("_query", SearchParamType.Token, new Uri(ResourceQuery)); _searchParameterInfos = new[] { new SearchParameterInfo("_id", SearchParamType.Token, new Uri(ResourceId), targetResourceTypes: targetResourceTypes, baseResourceTypes: baseResourceTypes), new SearchParameterInfo("_lastUpdated", SearchParamType.Token, new Uri(ResourceLastupdated), targetResourceTypes: targetResourceTypes, baseResourceTypes: baseResourceTypes), new SearchParameterInfo("_profile", SearchParamType.Token, new Uri(ResourceProfile), targetResourceTypes: targetResourceTypes), new SearchParameterInfo("_security", SearchParamType.Token, new Uri(ResourceSecurity), targetResourceTypes: targetResourceTypes), _queryParameter, }; _searchParameterDefinitionManager.GetSearchParameters("Account") .Returns(_searchParameterInfos); _searchParameterDefinitionManager.AllSearchParameters .Returns(_searchParameterInfos); _searchParameterDefinitionManager.GetSearchParameter(new Uri(ResourceQuery)) .Returns(_queryParameter); _searchParameterSupportResolver .IsSearchParameterSupported(Arg.Any <SearchParameterInfo>()) .Returns((false, false)); _searchParameterSupportResolver .IsSearchParameterSupported(Arg.Is(_searchParameterInfos[4])) .Returns((true, false)); }
private Expression ParseImpl(string resourceType, ReadOnlySpan <char> key, string value) { if (TryConsume(ReverseChainParameter.AsSpan(), ref key)) { if (!TrySplit(ChainSplitChar, ref key, out ReadOnlySpan <char> type)) { throw new InvalidSearchOperationException(Core.Resources.ReverseChainMissingType); } if (!TrySplit(ChainSplitChar, ref key, out ReadOnlySpan <char> refParam)) { throw new InvalidSearchOperationException(Core.Resources.ReverseChainMissingReference); } string typeString = type.ToString(); SearchParameterInfo refSearchParameter = _searchParameterDefinitionManager.GetSearchParameter(typeString, refParam.ToString()); return(ParseChainedExpression(typeString, refSearchParameter, resourceType, key, value, true)); } if (TrySplit(ChainParameter, ref key, out ReadOnlySpan <char> chainedInput)) { ReadOnlySpan <char> targetTypeName; if (TrySplit(ChainSplitChar, ref chainedInput, out ReadOnlySpan <char> refParamName)) { targetTypeName = chainedInput; } else { refParamName = chainedInput; targetTypeName = ReadOnlySpan <char> .Empty; } if (refParamName.IsEmpty) { throw new SearchParameterNotSupportedException(resourceType, key.ToString()); } SearchParameterInfo refSearchParameter = _searchParameterDefinitionManager.GetSearchParameter(resourceType, refParamName.ToString()); return(ParseChainedExpression(resourceType, refSearchParameter, targetTypeName.ToString(), key, value, false)); } ReadOnlySpan <char> modifier; if (TrySplit(ChainSplitChar, ref key, out ReadOnlySpan <char> paramName)) { modifier = key; } else { paramName = key; modifier = ReadOnlySpan <char> .Empty; } // Check to see if the search parameter is supported for this type or not. SearchParameterInfo searchParameter = _searchParameterDefinitionManager.GetSearchParameter(resourceType, paramName.ToString()); return(ParseSearchValueExpression(searchParameter, modifier.ToString(), value)); }