private (bool Supported, bool IsPartiallySupported) CheckSearchParameterSupport(SearchParameterInfo parameterInfo) { try { return(_searchParameterSupportResolver.IsSearchParameterSupported(parameterInfo)); } catch (Exception ex) { _logger.LogWarning("Unable to resolve search parameter {0}. Exception: {1}", parameterInfo?.Code, ex); return(false, false); } }
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); } }
private NormalizedSearchParameterQueryGenerator VisitSearchParameterExpressionBase(SearchParameterInfo searchParameterInfo, Expression childExpression, object context) { switch (searchParameterInfo.Name) { case SearchParameterNames.Id: case SearchParameterNames.LastUpdated: case SearchParameterNames.ResourceType: case SqlSearchParameters.ResourceSurrogateIdParameterName: // these are all denormalized return(null); } if (childExpression != null) { if (searchParameterInfo.Type == SearchParamType.Token) { // could be Token or TokenText return(childExpression.AcceptVisitor(this, context)); } } if (!_cache.TryGetValue(searchParameterInfo.Url, out var generator)) { generator = GetGenerator(searchParameterInfo); _cache.TryAdd(searchParameterInfo.Url, generator); } return(generator); NormalizedSearchParameterQueryGenerator GetGenerator(SearchParameterInfo param) { switch (param.Type) { case SearchParamType.Token: return(TokenSearchParameterQueryGenerator.Instance); case SearchParamType.Date: return(DateTimeSearchParameterQueryGenerator.Instance); case SearchParamType.Number: return(NumberSearchParameterQueryGenerator.Instance); case SearchParamType.Quantity: return(QuantitySearchParameterQueryGenerator.Instance); case SearchParamType.Reference: return(ReferenceSearchParameterQueryGenerator.Instance); case SearchParamType.String: return(StringSearchParameterQueryGenerator.Instance); case SearchParamType.Uri: return(UriSearchParameterQueryGenerator.Instance); case SearchParamType.Composite: Type searchValueType = _searchParameterToSearchValueTypeMap.GetSearchValueType(param); if (searchValueType == typeof(ValueTuple <TokenSearchValue, QuantitySearchValue>)) { return(TokenQuantityCompositeSearchParameterQueryGenerator.Instance); } if (searchValueType == typeof(ValueTuple <ReferenceSearchValue, TokenSearchValue>)) { return(ReferenceTokenCompositeSearchParameterQueryGenerator.Instance); } if (searchValueType == typeof(ValueTuple <TokenSearchValue, TokenSearchValue>)) { return(TokenTokenCompositeSearchParameterQueryGenerator.Instance); } if (searchValueType == typeof(ValueTuple <TokenSearchValue, DateTimeSearchValue>)) { return(TokenDateTimeCompositeSearchParameterQueryGenerator.Instance); } if (searchValueType == typeof(ValueTuple <TokenSearchValue, StringSearchValue>)) { return(TokenStringCompositeSearchParameterQueryGenerator.Instance); } if (searchValueType == typeof(ValueTuple <TokenSearchValue, NumberSearchValue, NumberSearchValue>)) { return(TokenNumberNumberSearchParameterQueryGenerator.Instance); } throw new InvalidOperationException($"Unexpected composite search parameter {param.Url}"); default: throw new InvalidOperationException($"Unexpected search parameter type {param.Type}"); } } }
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); _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, }, }; _searchParameterRegistry.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)); }
public (bool Supported, bool IsPartiallySupported) IsSearchParameterSupported(SearchParameterInfo parameterInfo) { EnsureArg.IsNotNull(parameterInfo, nameof(parameterInfo)); if (string.IsNullOrWhiteSpace(parameterInfo.Expression)) { return(false, false); } Expression parsed = _compiler.Parse(parameterInfo.Expression); if (parameterInfo.Component != null && parameterInfo.Component.Any(x => x.ResolvedSearchParameter == null)) { return(false, false); } (SearchParamType Type, Expression, Uri DefinitionUrl)[] componentExpressions = parameterInfo.Component
protected SearchParameterExpressionBase(SearchParameterInfo searchParameter) { Parameter = searchParameter; EnsureArg.IsNotNull(searchParameter, nameof(searchParameter)); }
private Expression ParseChainedExpression(string resourceType, SearchParameterInfo searchParameter, string targetResourceType, ReadOnlySpan <char> remainingKey, string value, bool reversed) { // We have more paths after this so this is a chained expression. // Since this is chained expression, the expression must be a reference type. if (searchParameter.Type != ValueSets.SearchParamType.Reference) { // The search parameter is not a reference type, which is not allowed. throw new InvalidSearchOperationException(Core.Resources.ChainedParameterMustBeReferenceSearchParamType); } // Check to see if the client has specifically specified the target resource type to scope to. if (!string.IsNullOrEmpty(targetResourceType)) { // A target resource type is specified. if (!ModelInfoProvider.IsKnownResource(targetResourceType)) { throw new InvalidSearchOperationException(string.Format(Core.Resources.ResourceNotSupported, targetResourceType)); } } ChainedExpression chainedExpression = null; foreach (var possibleTargetResourceType in searchParameter.TargetResourceTypes) { if (!string.IsNullOrEmpty(targetResourceType) && targetResourceType != possibleTargetResourceType) { continue; } var multipleChainType = reversed ? resourceType : possibleTargetResourceType; ChainedExpression expression; try { expression = Expression.Chained( resourceType, searchParameter, possibleTargetResourceType, reversed, ParseImpl( multipleChainType, remainingKey, value)); } catch (Exception ex) when(ex is ResourceNotSupportedException || ex is SearchParameterNotSupportedException) { // The resource or search parameter is not supported for the resource. // We will ignore these unsupported types. continue; } if (chainedExpression == null) { chainedExpression = expression; } else { // If the target resource type is ambiguous, we throw an error. // At the moment, this is not supported throw new InvalidSearchOperationException( string.Format( CultureInfo.CurrentCulture, Core.Resources.ChainedParameterSpecifyType, searchParameter.Name, string.Join(Core.Resources.OrDelimiter, searchParameter.TargetResourceTypes.Select(c => $"{searchParameter.Name}:{c}")))); } } if (chainedExpression == null) { // There was no reference that supports the search parameter. throw new InvalidSearchOperationException(Core.Resources.ChainedParameterNotSupported); } return(chainedExpression); }
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.StartsWith(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 bool TryGetSearchParameter(Uri definitionUri, out SearchParameterInfo value) { return(UrlLookup.TryGetValue(definitionUri, out value)); }
public void GivenACompositeExceedingNumberOfComponents_WhenBuilt_ThenInvalidSearchOperationExceptionShouldBeThrown() { SearchParameterInfo searchParameter = CreateCompositeSearchParameter(new SearchParameterComponentInfo()); Assert.Throws <InvalidSearchOperationException>(() => _parser.Parse(searchParameter, null, "a$b$c")); }
private Expression Build( SearchParameterInfo searchParameter, SearchModifierCode?modifier, int?componentIndex, string value) { ReadOnlySpan <char> valueSpan = value.AsSpan(); // By default, the comparator is equal. SearchComparator comparator = SearchComparator.Eq; if (searchParameter.Type == ValueSets.SearchParamType.Date || searchParameter.Type == ValueSets.SearchParamType.Number || searchParameter.Type == ValueSets.SearchParamType.Quantity) { // If the search parameter type supports comparator, parse the comparator (if present). Tuple <string, SearchComparator> matchedComparator = SearchParamComparators.FirstOrDefault( s => value.StartsWith(s.Item1, StringComparison.Ordinal)); if (matchedComparator != null) { comparator = matchedComparator.Item2; valueSpan = valueSpan.Slice(matchedComparator.Item1.Length); } } // Parse the value. Func <string, ISearchValue> parser = _parserDictionary[Enum.Parse <SearchParamType>(searchParameter.Type.ToString())]; // Build the expression. var helper = new SearchValueExpressionBuilderHelper(); // If the value contains comma, then we need to convert it into in expression. // But in this case, the user cannot specify prefix. IReadOnlyList <string> parts = value.SplitByOrSeparator(); if (parts.Count == 1) { // This is a single value expression. ISearchValue searchValue = parser(valueSpan.ToString()); return(helper.Build( searchParameter.Name, modifier, comparator, componentIndex, searchValue)); } else { if (comparator != SearchComparator.Eq) { throw new InvalidSearchOperationException(Core.Resources.SearchComparatorNotSupported); } // This is a multiple value expression. Expression[] expressions = parts.Select(part => { ISearchValue searchValue = parser(part); return(helper.Build( searchParameter.Name, modifier, comparator, componentIndex, searchValue)); }).ToArray(); return(Expression.Or(expressions)); } }
private static List <(string ResourceType, SearchParameterInfo SearchParameter)> ValidateAndGetFlattenedList( IReadOnlyCollection <ITypedElement> searchParamCollection, IDictionary <Uri, SearchParameterInfo> uriDictionary, IModelInfoProvider modelInfoProvider) { var issues = new List <OperationOutcomeIssue>(); var searchParameters = searchParamCollection.Select((x, entryIndex) => { try { return(new SearchParameterWrapper(x)); } catch (ArgumentException) { AddIssue(Core.Resources.SearchParameterDefinitionInvalidResource, entryIndex); return(null); } }).ToList(); // Do the first pass to make sure all resources are SearchParameter. for (int entryIndex = 0; entryIndex < searchParameters.Count; entryIndex++) { SearchParameterWrapper searchParameter = searchParameters[entryIndex]; if (searchParameter == null) { continue; } try { SearchParameterInfo searchParameterInfo = GetOrCreateSearchParameterInfo(searchParameter, uriDictionary); uriDictionary.Add(new Uri(searchParameter.Url), searchParameterInfo); } catch (FormatException) { AddIssue(Resources.SearchParameterDefinitionInvalidDefinitionUri, entryIndex); continue; } catch (ArgumentException) { AddIssue(Resources.SearchParameterDefinitionDuplicatedEntry, searchParameter.Url); continue; } } EnsureNoIssues(); var validatedSearchParameters = new List <(string ResourceType, SearchParameterInfo SearchParameter)> { // _type is currently missing from the search params definition bundle, so we inject it in here. (KnownResourceTypes.Resource, new SearchParameterInfo(SearchParameterNames.ResourceType, SearchParameterNames.ResourceType, SearchParamType.Token, SearchParameterNames.ResourceTypeUri, null, "Resource.type().name", null)), }; // Do the second pass to make sure the definition is valid. foreach (var searchParameter in searchParameters) { if (searchParameter == null) { continue; } // If this is a composite search parameter, then make sure components are defined. if (string.Equals(searchParameter.Type, SearchParamType.Composite.GetLiteral(), StringComparison.OrdinalIgnoreCase)) { if (modelInfoProvider.Version == FhirSpecification.R5 && _knownBrokenR5.Contains(new Uri(searchParameter.Url))) { continue; } var composites = searchParameter.Component; if (composites.Count == 0) { AddIssue(Core.Resources.SearchParameterDefinitionInvalidComponent, searchParameter.Url); continue; } SearchParameterInfo compositeSearchParameter = GetOrCreateSearchParameterInfo(searchParameter, uriDictionary); for (int componentIndex = 0; componentIndex < composites.Count; componentIndex++) { ITypedElement component = composites[componentIndex]; var definitionUrl = GetComponentDefinition(component); if (definitionUrl == null || !uriDictionary.TryGetValue(new Uri(definitionUrl), out SearchParameterInfo componentSearchParameter)) { AddIssue( Core.Resources.SearchParameterDefinitionInvalidComponentReference, searchParameter.Url, componentIndex); continue; } if (componentSearchParameter.Type == SearchParamType.Composite) { AddIssue( Core.Resources.SearchParameterDefinitionComponentReferenceCannotBeComposite, searchParameter.Url, componentIndex); continue; } if (string.IsNullOrWhiteSpace(component.Scalar("expression")?.ToString())) { AddIssue( Core.Resources.SearchParameterDefinitionInvalidComponentExpression, searchParameter.Url, componentIndex); continue; } compositeSearchParameter.Component[componentIndex].ResolvedSearchParameter = componentSearchParameter; } } // Make sure the base is defined. IReadOnlyList <string> bases = searchParameter.Base; if (bases.Count == 0) { AddIssue(Core.Resources.SearchParameterDefinitionBaseNotDefined, searchParameter.Url); continue; } for (int baseElementIndex = 0; baseElementIndex < bases.Count; baseElementIndex++) { var code = bases[baseElementIndex]; string baseResourceType = code; // Make sure the expression is not empty unless they are known to have empty expression. // These are special search parameters that searches across all properties and needs to be handled specially. if (ShouldExcludeEntry(baseResourceType, searchParameter.Name, modelInfoProvider) || (modelInfoProvider.Version == FhirSpecification.R5 && _knownBrokenR5.Contains(new Uri(searchParameter.Url)))) { continue; } else { if (string.IsNullOrWhiteSpace(searchParameter.Expression)) { AddIssue(Core.Resources.SearchParameterDefinitionInvalidExpression, searchParameter.Url); continue; } } validatedSearchParameters.Add((baseResourceType, GetOrCreateSearchParameterInfo(searchParameter, uriDictionary))); } } EnsureNoIssues(); return(validatedSearchParameters); void AddIssue(string format, params object[] args) { issues.Add(new OperationOutcomeIssue( OperationOutcomeConstants.IssueSeverity.Fatal, OperationOutcomeConstants.IssueType.Invalid, string.Format(CultureInfo.InvariantCulture, format, args))); } void EnsureNoIssues() { if (issues.Count != 0) { throw new InvalidDefinitionException( Core.Resources.SearchParameterDefinitionContainsInvalidEntry, issues.ToArray()); } } }
private bool UsePartialSearchParams(SearchParameterInfo parameter) { return(_fhirReqeustContextAccessor.RequestContext != null && _fhirReqeustContextAccessor.RequestContext.IncludePartiallyIndexedSearchParams && parameter.IsSupported); }
public SearchParamTableExpressionQueryGenerator GetSearchParamTableExpressionQueryGenerator(SearchParameterInfo searchParameterInfo) { return(VisitSearchParameterExpressionBase(searchParameterInfo, null, null)); }
private List <(string ResourceType, SearchParameterInfo SearchParameter)> ValidateAndGetFlattenedList() { var issues = new List <OperationOutcomeIssue>(); BundleWrapper bundle = null; using (Stream stream = _modelInfoProvider.OpenVersionedFileStream(_embeddedResourceName, _embeddedResourceNamespace, _assembly)) { using TextReader reader = new StreamReader(stream); using JsonReader jsonReader = new JsonTextReader(reader); try { bundle = new BundleWrapper(FhirJsonNode.Read(jsonReader).ToTypedElement(_modelInfoProvider.StructureDefinitionSummaryProvider)); } catch (FormatException ex) { AddIssue(ex.Message); } } EnsureNoIssues(); IReadOnlyList <BundleEntryWrapper> entries = bundle.Entries; // Do the first pass to make sure all resources are SearchParameter. for (int entryIndex = 0; entryIndex < entries.Count; entryIndex++) { // Make sure resources are not null and they are SearchParameter. BundleEntryWrapper entry = entries[entryIndex]; ITypedElement searchParameterElement = entry.Resource; if (searchParameterElement == null || !string.Equals(searchParameterElement.InstanceType, KnownResourceTypes.SearchParameter, StringComparison.OrdinalIgnoreCase)) { AddIssue(Core.Resources.SearchParameterDefinitionInvalidResource, entryIndex); continue; } var searchParameter = new SearchParameterWrapper(searchParameterElement); try { SearchParameterInfo searchParameterInfo = CreateSearchParameterInfo(searchParameter); _uriDictionary.Add(new Uri(searchParameter.Url), searchParameterInfo); } catch (FormatException) { AddIssue(Core.Resources.SearchParameterDefinitionInvalidDefinitionUri, entryIndex); continue; } catch (ArgumentException) { AddIssue(Core.Resources.SearchParameterDefinitionDuplicatedEntry, searchParameter.Url); continue; } } EnsureNoIssues(); var validatedSearchParameters = new List <(string ResourceType, SearchParameterInfo SearchParameter)> { // _type is currently missing from the search params definition bundle, so we inject it in here. (KnownResourceTypes.Resource, new SearchParameterInfo(SearchParameterNames.ResourceType, SearchParamType.Token, SearchParameterNames.ResourceTypeUri, null, "Resource.type().name", null)), }; // Do the second pass to make sure the definition is valid. for (int entryIndex = 0; entryIndex < entries.Count; entryIndex++) { BundleEntryWrapper entry = entries[entryIndex]; ITypedElement searchParameterElement = entry.Resource; var searchParameter = new SearchParameterWrapper(searchParameterElement); // If this is a composite search parameter, then make sure components are defined. if (string.Equals(searchParameter.Type, SearchParamType.Composite.GetLiteral(), StringComparison.OrdinalIgnoreCase)) { var composites = searchParameter.Component; if (composites.Count == 0) { AddIssue(Core.Resources.SearchParameterDefinitionInvalidComponent, searchParameter.Url); continue; } for (int componentIndex = 0; componentIndex < composites.Count; componentIndex++) { ITypedElement component = composites[componentIndex]; var definitionUrl = GetComponentDefinition(component); if (definitionUrl == null || !_uriDictionary.TryGetValue(new Uri(definitionUrl), out SearchParameterInfo componentSearchParameter)) { AddIssue( Core.Resources.SearchParameterDefinitionInvalidComponentReference, searchParameter.Url, componentIndex); continue; } if (componentSearchParameter.Type == SearchParamType.Composite) { AddIssue( Core.Resources.SearchParameterDefinitionComponentReferenceCannotBeComposite, searchParameter.Url, componentIndex); continue; } if (string.IsNullOrWhiteSpace(component.Scalar("expression")?.ToString())) { AddIssue( Core.Resources.SearchParameterDefinitionInvalidComponentExpression, searchParameter.Url, componentIndex); continue; } } } // Make sure the base is defined. var bases = searchParameter.Base; if (bases.Count == 0) { AddIssue(Core.Resources.SearchParameterDefinitionBaseNotDefined, searchParameter.Url); continue; } for (int baseElementIndex = 0; baseElementIndex < bases.Count; baseElementIndex++) { var code = bases[baseElementIndex]; string baseResourceType = code; // Make sure the expression is not empty unless they are known to have empty expression. // These are special search parameters that searches across all properties and needs to be handled specially. if (ShouldExcludeEntry(baseResourceType, searchParameter.Name) || (_modelInfoProvider.Version == FhirSpecification.R5 && _knownBrokenR5.Contains(new Uri(searchParameter.Url)))) { continue; } else { if (string.IsNullOrWhiteSpace(searchParameter.Expression)) { AddIssue(Core.Resources.SearchParameterDefinitionInvalidExpression, searchParameter.Url); continue; } } validatedSearchParameters.Add((baseResourceType, CreateSearchParameterInfo(searchParameter))); } } EnsureNoIssues(); return(validatedSearchParameters); void AddIssue(string format, params object[] args) { issues.Add(new OperationOutcomeIssue( OperationOutcomeConstants.IssueSeverity.Fatal, OperationOutcomeConstants.IssueType.Invalid, string.Format(CultureInfo.InvariantCulture, format, args))); } void EnsureNoIssues() { if (issues.Count != 0) { throw new InvalidDefinitionException( Core.Resources.SearchParameterDefinitionContainsInvalidEntry, issues.ToArray()); } } }
public SearchParamType GetSearchParameterType(SearchParameterInfo searchParameter, int?componentIndex) { return(_inner.GetSearchParameterType(searchParameter, componentIndex)); }
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 Expression ParseImpl(string[] resourceTypes, ReadOnlySpan <char> key, string value) { if (TryConsume(ReverseChainParameter.AsSpan(), ref key)) { if (!TrySplit(SearchSplitChar, ref key, out ReadOnlySpan <char> type)) { throw new InvalidSearchOperationException(Core.Resources.ReverseChainMissingType); } if (!TrySplit(SearchSplitChar, 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(new[] { typeString }, refSearchParameter, resourceTypes, key, value, true)); } if (TrySplit(ChainParameter, ref key, out ReadOnlySpan <char> chainedInput)) { string[] targetType = Array.Empty <string>(); if (TrySplit(SearchSplitChar, ref chainedInput, out ReadOnlySpan <char> refParamName)) { targetType = new[] { chainedInput.ToString() }; } else { refParamName = chainedInput; } if (refParamName.IsEmpty) { throw new SearchParameterNotSupportedException(resourceTypes[0], key.ToString()); } SearchParameterInfo refSearchParameter = _searchParameterDefinitionManager.GetSearchParameter(resourceTypes[0], refParamName.ToString()); foreach (var resourceType in resourceTypes) { if (refSearchParameter != _searchParameterDefinitionManager.GetSearchParameter(resourceType, refParamName.ToString())) { throw new BadRequestException(string.Format(Core.Resources.SearchParameterMustBeCommon, refParamName.ToString(), resourceTypes[0], resourceType)); } } return(ParseChainedExpression(resourceTypes, refSearchParameter, targetType, key, value, false)); } ReadOnlySpan <char> modifier; if (TrySplit(SearchSplitChar, 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(resourceTypes[0], paramName.ToString()); foreach (var resourceType in resourceTypes) { if (searchParameter != _searchParameterDefinitionManager.GetSearchParameter(resourceType, paramName.ToString())) { throw new BadRequestException(string.Format(Core.Resources.SearchParameterMustBeCommon, paramName.ToString(), resourceTypes[0], resourceType)); } } return(ParseSearchValueExpression(searchParameter, modifier.ToString(), value)); }
public async Task GivenNewSearchParam_WhenReindexJobCompleted_ThenParamIsSearchable() { var searchParamName = "foo"; var searchParam = new SearchParameterInfo( name: searchParamName, searchParamType: ValueSets.SearchParamType.String, url: new Uri("http://hl7.org/fhir/SearchParameter/Patient-foo"), components: null, expression: "Patient.name", targetResourceTypes: null, baseResourceTypes: new List <string>() { "Patient" }) { IsSupported = true, IsSearchable = false, }; _searchParameterDefinitionManager.UrlLookup.Add(searchParam.Url, searchParam); _searchParameterDefinitionManager.TypeLookup["Patient"].Add(searchParamName, searchParam); await UpsertPatientData("searchIndicesPatient1"); await UpsertPatientData("searchIndicesPatient2"); var queryParams = new List <Tuple <string, string> >() { new Tuple <string, string>("foo", "searchIndicesPatient1") }; var searchResults = await _searchService.Value.SearchAsync("Patient", queryParams, CancellationToken.None); Assert.Equal(searchParamName, searchResults.UnsupportedSearchParameters.FirstOrDefault().Item1); Assert.Equal(2, searchResults.Results.Count()); var searchIndexValues1 = new List <SearchIndexEntry>(); searchIndexValues1.Add(new SearchIndexEntry(searchParam, new StringSearchValue("searchIndicesPatient1"))); _searchIndexer.Extract(Arg.Is <ResourceElement>(r => r.Id.Equals("searchIndicesPatient1"))).Returns(searchIndexValues1); var searchIndexValues2 = new List <SearchIndexEntry>(); searchIndexValues2.Add(new SearchIndexEntry(searchParam, new StringSearchValue("searchIndicesPatient2"))); _searchIndexer.Extract(Arg.Is <ResourceElement>(r => r.Id.Equals("searchIndicesPatient2"))).Returns(searchIndexValues2); var request = new CreateReindexRequest(); CreateReindexResponse response = await _createReindexRequestHandler.Handle(request, CancellationToken.None); Assert.NotNull(response); Assert.False(string.IsNullOrWhiteSpace(response.Job.JobRecord.Id)); _reindexJobWorker = new ReindexJobWorker( () => _scopedOperationDataStore, Options.Create(_jobConfiguration), InitializeReindexJobTask, NullLogger <ReindexJobWorker> .Instance); var cancellationTokenSource = new CancellationTokenSource(); try { var reindexWorkerTask = _reindexJobWorker.ExecuteAsync(cancellationTokenSource.Token); var reindexJobWrapper = await _fhirOperationDataStore.GetReindexJobByIdAsync(response.Job.JobRecord.Id, cancellationTokenSource.Token); int delayCount = 0; while (reindexJobWrapper.JobRecord.Status != OperationStatus.Completed && delayCount < 10) { await Task.Delay(1000); delayCount++; reindexJobWrapper = await _fhirOperationDataStore.GetReindexJobByIdAsync(response.Job.JobRecord.Id, cancellationTokenSource.Token); } Assert.True(delayCount <= 9); searchResults = await _searchService.Value.SearchAsync("Patient", queryParams, CancellationToken.None); Assert.Single(searchResults.Results); var patient = searchResults.Results.FirstOrDefault().Resource; Assert.Contains("searchIndicesPatient1", patient.RawResource.Data); } finally { cancellationTokenSource.Cancel(); } }
/// <summary> /// Creates a <see cref="SearchParameterExpression"/> that represents a set of ANDed expressions over a search parameter. /// </summary> /// <param name="searchParameter">The search parameter this expression is bound to.</param> /// <param name="expression">The expression over the parameter's values.</param> /// <returns>A <see cref="SearchParameterExpression"/>.</returns> public static SearchParameterExpression SearchParameter(SearchParameterInfo searchParameter, Expression expression) { return(new SearchParameterExpression(searchParameter, expression)); }
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)); }
/// <summary> /// Creates a <see cref="MissingSearchParameterExpression"/> that represents a search parameter being present or not in a resource. /// </summary> /// <param name="searchParameter">The search parameter this expression is bound to.</param> /// <param name="isMissing">A flag indicating whether the parameter should be missing or not.</param> /// <returns>A <see cref="SearchParameterExpression"/>.</returns> public static MissingSearchParameterExpression MissingSearchParameter(SearchParameterInfo searchParameter, bool isMissing) { return(new MissingSearchParameterExpression(searchParameter, isMissing)); }
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)); } 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)); } } }
/// <summary> /// Creates a <see cref="ChainedExpression"/> that represents chained operation. /// </summary> /// <param name="resourceType">The resource type.</param> /// <param name="referenceSearchParameter">The search parameter that establishes the reference between resources</param> /// <param name="targetResourceType">The target resource type.</param> /// <param name="reversed">If this is a reversed chained expression.</param> /// <param name="expression">The expression.</param> /// <returns>A <see cref="ChainedExpression"/> that represents chained operation on <paramref name="targetResourceType"/> through <paramref name="referenceSearchParameter"/>.</returns> public static ChainedExpression Chained(string resourceType, SearchParameterInfo referenceSearchParameter, string targetResourceType, bool reversed, Expression expression) { return(new ChainedExpression(resourceType, referenceSearchParameter, targetResourceType, reversed, expression)); }
public SearchParameterDefinitionManagerTests() { _searchParameterSupportResolver = Substitute.For <ISearchParameterSupportResolver>(); _mediator = Substitute.For <IMediator>(); _searchParameterRegistry = Substitute.For <ISearchParameterRegistry>(); _searchParameterDefinitionManager = new SearchParameterDefinitionManager(ModelInfoProvider.Instance); _fhirRequestContextAccessor = Substitute.For <IFhirRequestContextAccessor>(); _fhirRequestContextAccessor.FhirRequestContext.Returns(_fhirRequestContext); _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, }; _testSearchParamInfo = new SearchParameterInfo("_test", SearchParamType.Special, new Uri(ResourceTest)); _searchParameterSupportResolver .IsSearchParameterSupported(Arg.Any <SearchParameterInfo>()) .Returns((false, false)); _searchParameterSupportResolver .IsSearchParameterSupported(Arg.Is(_searchParameterInfos[4])) .Returns((true, false)); _searchParameterDefinitionManager.Start(); }
/// <summary> /// Creates a <see cref="IncludeExpression"/> that represents an include operation. /// </summary> /// <param name="resourceType">The source resource type.</param> /// <param name="referenceSearchParameter">The search parameter that establishes the reference between resources</param> /// <param name="targetResourceType">The target resource type.</param> /// <param name="wildCard">If this is a wildcard include.</param> /// <param name="reversed">If this is a reversed include (revinclude) expression.</param> /// <returns>A <see cref="IncludeExpression"/> that represents an include on <param name="targetResourceType"> through <paramref name="referenceSearchParameter"/>.</param></returns> public static IncludeExpression Include(string resourceType, SearchParameterInfo referenceSearchParameter, string targetResourceType, bool wildCard, bool reversed) { return(new IncludeExpression(resourceType, referenceSearchParameter, targetResourceType, wildCard, reversed)); }
private List <(string ResourceType, SearchParameterInfo SearchParameter)> ValidateAndGetFlattenedList() { var issues = new List <OperationOutcomeIssue>(); Bundle bundle = null; using (Stream stream = _assembly.GetManifestResourceStream(_embeddedResourceName)) { using TextReader reader = new StreamReader(stream); using JsonReader jsonReader = new JsonTextReader(reader); try { // The parser does some basic validation such as making sure entry is not null, enum has the right value, and etc. bundle = _fhirJsonParser.Parse <Bundle>(jsonReader); } catch (FormatException ex) { AddIssue(ex.Message); } } EnsureNoIssues(); // Do the first pass to make sure all resources are SearchParameter. for (int entryIndex = 0; entryIndex < bundle.Entry.Count; entryIndex++) { // Make sure resources are not null and they are SearchParameter. EntryComponent entry = bundle.Entry[entryIndex]; var searchParameter = entry.Resource as SearchParameter; if (searchParameter == null) { AddIssue(Core.Resources.SearchParameterDefinitionInvalidResource, entryIndex); continue; } try { SearchParameterInfo searchParameterInfo = CreateSearchParameterInfo(searchParameter); _uriDictionary.Add(new Uri(searchParameter.Url), searchParameterInfo); } catch (FormatException) { AddIssue(Core.Resources.SearchParameterDefinitionInvalidDefinitionUri, entryIndex); continue; } catch (ArgumentException) { AddIssue(Core.Resources.SearchParameterDefinitionDuplicatedEntry, searchParameter.Url); continue; } } EnsureNoIssues(); var validatedSearchParameters = new List <(string ResourceType, SearchParameterInfo SearchParameter)> { // _type is currently missing from the search params definition bundle, so we inject it in here. (ResourceType.Resource.ToString(), new SearchParameterInfo(SearchParameterNames.ResourceType, SearchParamType.Token.ToString(), SearchParameterNames.ResourceTypeUri, null, "Resource.type().name", null)), }; // Do the second pass to make sure the definition is valid. for (int entryIndex = 0; entryIndex < bundle.Entry.Count; entryIndex++) { var searchParameter = (SearchParameter)bundle.Entry[entryIndex].Resource; // If this is a composite search parameter, then make sure components are defined. if (searchParameter.Type == SearchParamType.Composite) { if (searchParameter.Component?.Count == 0) { AddIssue(Core.Resources.SearchParameterDefinitionInvalidComponent, searchParameter.Url); continue; } for (int componentIndex = 0; componentIndex < searchParameter.Component.Count; componentIndex++) { ComponentComponent component = searchParameter.Component[componentIndex]; if (component.GetComponentDefinitionUri() == null || !_uriDictionary.TryGetValue(component.GetComponentDefinitionUri(), out SearchParameterInfo componentSearchParameter)) { AddIssue( Core.Resources.SearchParameterDefinitionInvalidComponentReference, searchParameter.Url, componentIndex); continue; } if (componentSearchParameter.Type == SearchParamType.Composite.ToValueSet()) { AddIssue( Core.Resources.SearchParameterDefinitionComponentReferenceCannotBeComposite, searchParameter.Url, componentIndex); continue; } if (string.IsNullOrWhiteSpace(component.Expression)) { AddIssue( Core.Resources.SearchParameterDefinitionInvalidComponentExpression, searchParameter.Url, componentIndex); continue; } } } // Make sure the base is defined. if (searchParameter.BaseElement?.Count == 0) { AddIssue(Core.Resources.SearchParameterDefinitionBaseNotDefined, searchParameter.Url); continue; } for (int baseElementIndex = 0; baseElementIndex < searchParameter.BaseElement.Count; baseElementIndex++) { Code <ResourceType> code = searchParameter.BaseElement[baseElementIndex]; string baseResourceType = code.Value.Value.ToString(); // Make sure the expression is not empty unless they are known to have empty expression. // These are special search parameters that searches across all properties and needs to be handled specially. if (ShouldExcludeEntry(baseResourceType, searchParameter.Name)) { continue; } else { if (string.IsNullOrWhiteSpace(searchParameter.Expression)) { AddIssue(Core.Resources.SearchParameterDefinitionInvalidExpression, searchParameter.Url); continue; } } validatedSearchParameters.Add((baseResourceType, CreateSearchParameterInfo(searchParameter))); } } EnsureNoIssues(); return(validatedSearchParameters); void AddIssue(string format, params object[] args) { issues.Add(new OperationOutcomeIssue( OperationOutcomeConstants.IssueSeverity.Fatal, OperationOutcomeConstants.IssueType.Invalid, string.Format(CultureInfo.InvariantCulture, format, args))); } void EnsureNoIssues() { if (issues.Count != 0) { throw new InvalidDefinitionException( Core.Resources.SearchParameterDefinitionContainsInvalidEntry, issues.ToArray()); } } }
public SearchParameterDefinitionManagerTests() { _searchParameterSupportResolver = Substitute.For <ISearchParameterSupportResolver>(); _mediator = Substitute.For <IMediator>(); _searchParameterStatusDataStore = Substitute.For <ISearchParameterStatusDataStore>(); _searchParameterDefinitionManager = new SearchParameterDefinitionManager(ModelInfoProvider.Instance, _mediator); _fhirRequestContextAccessor = Substitute.For <IFhirRequestContextAccessor>(); _fhirRequestContextAccessor.FhirRequestContext.Returns(_fhirRequestContext); _manager = new SearchParameterStatusManager( _searchParameterStatusDataStore, _searchParameterDefinitionManager, _searchParameterSupportResolver, _mediator); _searchParameterStatusDataStore.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", "_query", SearchParamType.Token, new Uri(ResourceQuery), baseResourceTypes: new List <string>() { "Patient" }); _searchParameterInfos = new[] { new SearchParameterInfo("_id", "_id", SearchParamType.Token, new Uri(ResourceId)), new SearchParameterInfo("_lastUpdated", "_lastUpdated", SearchParamType.Token, new Uri(ResourceLastUpdated)), new SearchParameterInfo("_profile", "_profile", SearchParamType.Token, new Uri(ResourceProfile)), new SearchParameterInfo("_security", "_security", SearchParamType.Token, new Uri(ResourceSecurity)), _queryParameter, }; _testSearchParamInfo = new SearchParameterInfo("_test", "_test", SearchParamType.Special, new Uri(ResourceTest)); _searchParameterSupportResolver .IsSearchParameterSupported(Arg.Any <SearchParameterInfo>()) .Returns((false, false)); _searchParameterSupportResolver .IsSearchParameterSupported(Arg.Is(_searchParameterInfos[4])) .Returns((true, false)); var searchParameterDataStoreValidator = Substitute.For <IDataStoreSearchParameterValidator>(); searchParameterDataStoreValidator.ValidateSearchParameter(Arg.Any <SearchParameterInfo>(), out Arg.Any <string>()).Returns(true, null); _searchParameterOperations = new SearchParameterOperations(_manager, _searchParameterDefinitionManager, ModelInfoProvider.Instance, _searchParameterSupportResolver, searchParameterDataStoreValidator); }
private static void ValidateSearchParam(SearchParameterInfo expectedSearchParam, SearchParameterInfo actualSearchParam) { Assert.Equal(expectedSearchParam.Name, actualSearchParam.Name); Assert.Equal(expectedSearchParam.Type, actualSearchParam.Type); Assert.Equal(expectedSearchParam.Url, actualSearchParam.Url); }
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)); } } }