Initial stub out of the CR repo API (#4627)

* Initial stub out of the CR repo API

* Pagination first pass

* cleanup

* add transaction tests

* add search param conversion working logic

* add result parameter conversion

* add test for keymap

* add type checking logic and tests

* remove and / or logic from test and cleanup

* Initial stub out of the CR repo API

* Pagination first pass

* cleanup

* add transaction tests

* add search param conversion working logic

* add result parameter conversion

* add test for keymap

* add type checking logic and tests

* Fix repository tests to use parameters

* Cleanup after latest merge

* Cleanup

* Cleanup

* Fix Msg codes

* Fix Msg codes

* Rename variable for clinical reasoning module

* Update clinical-reasoning version

* Fix version

* review comments

---------

Co-authored-by: Brenin Rhodes <brenin@alphora.com>
Co-authored-by: Rosie Elphick <rosalie.elphick@smilecdr.com>
This commit is contained in:
JP 2023-04-06 13:53:29 -06:00 committed by GitHub
parent 02556187a3
commit 182ac3b36c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1216 additions and 13 deletions

View File

@ -0,0 +1,4 @@
---
type: add
issue: 4695
title: "Add an implementation of the Clinical Reasoning FHIR Repository to allow calling the available CR operations in hapi-fhir."

View File

@ -87,7 +87,7 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
protected static ISearchCoordinatorSvc mySearchCoordinatorSvc;
protected static Server ourServer;
protected static JpaCapabilityStatementProvider ourCapabilityStatementProvider;
private static DatabaseBackedPagingProvider ourPagingProvider;
protected static DatabaseBackedPagingProvider ourPagingProvider;
private static GenericWebApplicationContext ourWebApplicationContext;
protected IGenericClient myClient;
@Autowired

View File

@ -91,7 +91,7 @@ public abstract class RequestDetails {
/**
* Copy constructor
*/
public RequestDetails(ServletRequestDetails theRequestDetails) {
public RequestDetails(RequestDetails theRequestDetails) {
myInterceptorBroadcaster = theRequestDetails.getInterceptorBroadcaster();
myRequestStopwatch = theRequestDetails.getRequestStopwatch();
myTenantId = theRequestDetails.getTenantId();

View File

@ -42,6 +42,8 @@ import java.io.Reader;
import java.nio.charset.Charset;
import java.util.List;
import static java.util.Objects.nonNull;
/**
* A default RequestDetails implementation that can be used for system calls to
* Resource DAO methods when partitioning is enabled. Using a SystemRequestDetails
@ -56,14 +58,21 @@ public class SystemRequestDetails extends RequestDetails {
*/
private RequestPartitionId myRequestPartitionId;
private IRestfulServerDefaults myServer = new MyRestfulServerDefaults();
public SystemRequestDetails() {
super(new MyInterceptorBroadcaster());
this(new MyInterceptorBroadcaster());
}
public SystemRequestDetails(IInterceptorBroadcaster theInterceptorBroadcaster) {
super(theInterceptorBroadcaster);
}
public SystemRequestDetails(RequestDetails theDetails) {
super(theDetails);
if (nonNull(theDetails.getServer())) { myServer = theDetails.getServer(); }
}
public RequestPartitionId getRequestPartitionId() {
return myRequestPartitionId;
}
@ -140,7 +149,7 @@ public class SystemRequestDetails extends RequestDetails {
@Override
public IRestfulServerDefaults getServer() {
return new MyRestfulServerDefaults();
return myServer;
}
@Override

View File

@ -201,7 +201,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
/**
* @since 5.5.0
*/
protected ConformanceMethodBinding getServerConformanceMethod() {
public ConformanceMethodBinding getServerConformanceMethod() {
return myServerConformanceMethod;
}

View File

@ -80,10 +80,15 @@
<artifactId>hapi-fhir-converter</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.opencds.cqf.fhir</groupId>
<artifactId>cqf-fhir-api</artifactId>
<version>${clinical-reasoning.version}</version>
</dependency>
<dependency>
<groupId>org.opencds.cqf.cql</groupId>
<artifactId>evaluator.fhir</artifactId>
<version>${cql-evaluator.version}</version>
<version>${clinical-reasoning.version}</version>
<exclusions>
<exclusion>
<groupId>ca.uhn.hapi.fhir</groupId>
@ -114,7 +119,7 @@
<dependency>
<groupId>org.opencds.cqf.cql</groupId>
<artifactId>evaluator.spring</artifactId>
<version>${cql-evaluator.version}</version>
<version>${clinical-reasoning.version}</version>
<exclusions>
<exclusion>
<groupId>xpp3</groupId>
@ -129,7 +134,7 @@
<dependency>
<groupId>org.opencds.cqf.cql</groupId>
<artifactId>evaluator.jackson-deps</artifactId>
<version>${cql-evaluator.version}</version>
<version>${clinical-reasoning.version}</version>
<type>pom</type>
<exclusions>
<exclusion>
@ -208,6 +213,10 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -69,6 +69,7 @@ import org.opencds.cqf.cql.evaluator.engine.model.CachingModelResolverDecorator;
import org.opencds.cqf.cql.evaluator.engine.retrieve.BundleRetrieveProvider;
import org.opencds.cqf.cql.evaluator.fhir.Constants;
import org.opencds.cqf.cql.evaluator.fhir.adapter.AdapterFactory;
import org.opencds.cqf.cql.evaluator.fhir.Constants;
import org.opencds.cqf.cql.evaluator.measure.MeasureEvaluationOptions;
import org.opencds.cqf.cql.evaluator.spring.fhir.adapter.AdapterConfiguration;
import org.slf4j.Logger;

View File

@ -0,0 +1,263 @@
package ca.uhn.fhir.cr.repo;
/*-
* #%L
* HAPI FHIR - Clinical Reasoning
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.rest.api.BundleLinks;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.IVersionSpecificBundleFactory;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.IRestfulServer;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.IPagingProvider;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseResource;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/**
* This class pulls existing methods from the BaseResourceReturningMethodBinding class used for taking
* the results of a BundleProvider and turning it into a Bundle. It is intended to be used only by the
* HapiFhirRepository.
*/
public class BundleProviderUtil {
private static final org.slf4j.Logger ourLog =
org.slf4j.LoggerFactory.getLogger(BaseResourceReturningMethodBinding.class);
public static IBaseResource createBundleFromBundleProvider(IRestfulServer<?> theServer,
RequestDetails theRequest, Integer theLimit, String theLinkSelf, Set<Include> theIncludes,
IBundleProvider theResult, int theOffset, BundleTypeEnum theBundleType,
EncodingEnum theLinkEncoding, String theSearchId) {
IVersionSpecificBundleFactory bundleFactory = theServer.getFhirContext().newBundleFactory();
final Integer offset;
Integer limit = theLimit;
if (theResult.getCurrentPageOffset() != null) {
offset = theResult.getCurrentPageOffset();
limit = theResult.getCurrentPageSize();
Validate.notNull(limit,
"IBundleProvider returned a non-null offset, but did not return a non-null page size");
} else {
offset = RestfulServerUtils.tryToExtractNamedParameter(theRequest, Constants.PARAM_OFFSET);
}
int numToReturn;
String searchId = null;
List<IBaseResource> resourceList;
Integer numTotalResults = theResult.size();
int pageSize;
if (offset != null || !theServer.canStoreSearchResults()) {
if (limit != null) {
pageSize = limit;
} else {
if (theServer.getDefaultPageSize() != null) {
pageSize = theServer.getDefaultPageSize();
} else {
pageSize = numTotalResults != null ? numTotalResults : Integer.MAX_VALUE;
}
}
numToReturn = pageSize;
if (offset != null || theResult.getCurrentPageOffset() != null) {
// When offset query is done theResult already contains correct amount (+ their includes
// etc.) so return everything
resourceList = theResult.getResources(0, Integer.MAX_VALUE);
} else if (numToReturn > 0) {
resourceList = theResult.getResources(0, numToReturn);
} else {
resourceList = Collections.emptyList();
}
RestfulServerUtils.validateResourceListNotNull(resourceList);
} else {
IPagingProvider pagingProvider = theServer.getPagingProvider();
if (limit == null || ((Integer) limit).equals(0)) {
pageSize = pagingProvider.getDefaultPageSize();
} else {
pageSize = Math.min(pagingProvider.getMaximumPageSize(), limit);
}
numToReturn = pageSize;
if (numTotalResults != null) {
numToReturn = Math.min(numToReturn, numTotalResults - theOffset);
}
if (numToReturn > 0 || theResult.getCurrentPageId() != null) {
resourceList = theResult.getResources(theOffset, numToReturn + theOffset);
} else {
resourceList = Collections.emptyList();
}
RestfulServerUtils.validateResourceListNotNull(resourceList);
if (numTotalResults == null) {
numTotalResults = theResult.size();
}
if (theSearchId != null) {
searchId = theSearchId;
} else {
if (numTotalResults == null || numTotalResults > numToReturn) {
searchId = pagingProvider.storeResultList(theRequest, theResult);
if (isBlank(searchId)) {
ourLog.info(
"Found {} results but paging provider did not provide an ID to use for paging",
numTotalResults);
searchId = null;
}
}
}
}
/*
* Remove any null entries in the list - This generally shouldn't happen but can if data has
* been manually purged from the JPA database
*/
boolean hasNull = false;
for (IBaseResource next : resourceList) {
if (next == null) {
hasNull = true;
break;
}
}
if (hasNull) {
resourceList.removeIf(Objects::isNull);
}
/*
* Make sure all returned resources have an ID (if not, this is a bug in the user server code)
*/
for (IBaseResource next : resourceList) {
if (next.getIdElement() == null || next.getIdElement().isEmpty()) {
if (!(next instanceof IBaseOperationOutcome)) {
throw new InternalErrorException(Msg.code(2311)
+ "Server method returned resource of type[" + next.getClass().getSimpleName()
+ "] with no ID specified (IResource#setId(IdDt) must be called)");
}
}
}
BundleLinks links = new BundleLinks(theRequest.getFhirServerBase(), theIncludes,
RestfulServerUtils.prettyPrintResponse(theServer, theRequest), theBundleType);
links.setSelf(theLinkSelf);
if (theResult.getCurrentPageOffset() != null) {
if (isNotBlank(theResult.getNextPageId())) {
links.setNext(RestfulServerUtils.createOffsetPagingLink(links,
theRequest.getRequestPath(), theRequest.getTenantId(), offset + limit, limit,
theRequest.getParameters()));
}
if (isNotBlank(theResult.getPreviousPageId())) {
links.setNext(RestfulServerUtils.createOffsetPagingLink(links,
theRequest.getRequestPath(), theRequest.getTenantId(),
Math.max(offset - limit, 0), limit, theRequest.getParameters()));
}
}
if (offset != null
|| (!theServer.canStoreSearchResults() && !isEverythingOperation(theRequest))) {
// Paging without caching
// We're doing offset pages
int requestedToReturn = numToReturn;
if (theServer.getPagingProvider() == null && offset != null) {
// There is no paging provider at all, so assume we're querying up to all the results we
// need every time
requestedToReturn += offset;
}
if (numTotalResults == null || requestedToReturn < numTotalResults) {
if (!resourceList.isEmpty()) {
links.setNext(
RestfulServerUtils.createOffsetPagingLink(links, theRequest.getRequestPath(),
theRequest.getTenantId(), defaultIfNull(offset, 0) + numToReturn,
numToReturn, theRequest.getParameters()));
}
}
if (offset != null && offset > 0) {
int start = Math.max(0, theOffset - pageSize);
links.setPrev(
RestfulServerUtils.createOffsetPagingLink(links, theRequest.getRequestPath(),
theRequest.getTenantId(), start, pageSize, theRequest.getParameters()));
}
} else if (isNotBlank(theResult.getCurrentPageId())) {
// We're doing named pages
searchId = theResult.getUuid();
if (isNotBlank(theResult.getNextPageId())) {
links.setNext(RestfulServerUtils.createPagingLink(links, theRequest, searchId,
theResult.getNextPageId(), theRequest.getParameters()));
}
if (isNotBlank(theResult.getPreviousPageId())) {
links.setPrev(RestfulServerUtils.createPagingLink(links, theRequest, searchId,
theResult.getPreviousPageId(), theRequest.getParameters()));
}
} else if (searchId != null) {
/*
* We're doing offset pages - Note that we only return paging links if we actually included
* some results in the response. We do this to avoid situations where people have faked the
* offset number to some huge number to avoid them getting back paging links that don't
* make sense.
*/
if (resourceList.size() > 0) {
if (numTotalResults == null || theOffset + numToReturn < numTotalResults) {
links.setNext((RestfulServerUtils.createPagingLink(links, theRequest, searchId,
theOffset + numToReturn, numToReturn, theRequest.getParameters())));
}
if (theOffset > 0) {
int start = Math.max(0, theOffset - pageSize);
links.setPrev(RestfulServerUtils.createPagingLink(links, theRequest, searchId, start,
pageSize, theRequest.getParameters()));
}
}
}
bundleFactory.addRootPropertiesToBundle(theResult.getUuid(), links, theResult.size(),
theResult.getPublished());
bundleFactory.addResourcesToBundle(new ArrayList<>(resourceList), theBundleType,
links.serverBase, theServer.getBundleInclusionRule(), theIncludes);
return bundleFactory.getResourceBundle();
}
private static boolean isEverythingOperation(RequestDetails theRequest) {
return (theRequest.getRestOperationType() == RestOperationTypeEnum.EXTENDED_OPERATION_TYPE
|| theRequest
.getRestOperationType() == RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE)
&& theRequest.getOperation() != null && theRequest.getOperation().equals("$everything");
}
}

View File

@ -0,0 +1,340 @@
package ca.uhn.fhir.cr.repo;
/*-
* #%L
* HAPI FHIR - Clinical Reasoning
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import static ca.uhn.fhir.cr.repo.RequestDetailsCloner.startWith;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseConformance;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.opencds.cqf.fhir.api.Repository;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.method.PageMethodBinding;
import ca.uhn.fhir.util.UrlUtil;
/**
* This class leverages DaoRegistry from Hapi-fhir to implement CRUD FHIR API operations constrained to provide only the operations necessary for the cql-evaluator modules to function.
**/
public class HapiFhirRepository implements Repository {
private static final org.slf4j.Logger ourLog =
org.slf4j.LoggerFactory.getLogger(HapiFhirRepository.class);
private final DaoRegistry myDaoRegistry;
private final RequestDetails myRequestDetails;
private final RestfulServer myRestfulServer;
public HapiFhirRepository(DaoRegistry theDaoRegistry, RequestDetails theRequestDetails,
RestfulServer theRestfulServer) {
myDaoRegistry = theDaoRegistry;
myRequestDetails = theRequestDetails;
myRestfulServer = theRestfulServer;
}
@Override
public <T extends IBaseResource, I extends IIdType> T read(Class<T> theResourceType, I theId,
Map<String, String> theHeaders) {
var details = startWith(myRequestDetails).addHeaders(theHeaders).create();
return myDaoRegistry.getResourceDao(theResourceType).read(theId, details);
}
@Override
public <T extends IBaseResource> MethodOutcome create(T theResource,
Map<String, String> theHeaders) {
var details = startWith(myRequestDetails).addHeaders(theHeaders).create();
return myDaoRegistry.getResourceDao(theResource).create(theResource, details);
}
@Override
public <I extends IIdType, P extends IBaseParameters> MethodOutcome patch(I theId,
P thePatchParameters, Map<String, String> theHeaders) {
var details = startWith(myRequestDetails).addHeaders(theHeaders).create();
// TODO: conditional url, patch type, patch body?
return myDaoRegistry.getResourceDao(theId.getResourceType()).patch(theId, null, null,
null, thePatchParameters, details);
}
@Override
public <T extends IBaseResource> MethodOutcome update(T theResource,
Map<String, String> theHeaders) {
var details = startWith(myRequestDetails).addHeaders(theHeaders).create();
return myDaoRegistry.getResourceDao(theResource).update(theResource, details);
}
@Override
public <T extends IBaseResource, I extends IIdType> MethodOutcome delete(
Class<T> theResourceType, I theId, Map<String, String> theHeaders) {
var details = startWith(myRequestDetails).addHeaders(theHeaders).create();
return myDaoRegistry.getResourceDao(theResourceType).delete(theId, details);
}
@Override
public <B extends IBaseBundle, T extends IBaseResource> B search(Class<B> theBundleType,
Class<T> theResourceType, Map<String, List<IQueryParameterType>> theSearchParameters,
Map<String, String> theHeaders) {
var details = startWith(myRequestDetails).addHeaders(theHeaders).create();
SearchConverter converter = new SearchConverter();
converter.convertParameters(theSearchParameters, fhirContext());
details.setParameters(converter.resultParameters);
var bundleProvider = myDaoRegistry.getResourceDao(theResourceType)
.search(converter.searchParameterMap, details);
if (bundleProvider == null) {
return null;
}
return createBundle(details, bundleProvider, null);
}
private <B extends IBaseBundle> B createBundle(RequestDetails theRequestDetails,
IBundleProvider theBundleProvider, String thePagingAction) {
var count = RestfulServerUtils.extractCountParameter(theRequestDetails);
var linkSelf = RestfulServerUtils.createLinkSelf(theRequestDetails.getFhirServerBase(),
theRequestDetails);
Set<Include> includes = new HashSet<>();
var reqIncludes = theRequestDetails.getParameters().get(Constants.PARAM_INCLUDE);
if (reqIncludes != null) {
for (String nextInclude : reqIncludes) {
includes.add(new Include(nextInclude));
}
}
var offset = RestfulServerUtils.tryToExtractNamedParameter(theRequestDetails,
Constants.PARAM_PAGINGOFFSET);
if (offset == null || offset < 0) {
offset = 0;
}
var start = offset;
if (theBundleProvider.size() != null) {
start = Math.max(0, Math.min(offset, theBundleProvider.size()));
}
BundleTypeEnum bundleType = null;
var bundleTypeValues = theRequestDetails.getParameters().get(Constants.PARAM_BUNDLETYPE);
if (bundleTypeValues != null) {
bundleType = BundleTypeEnum.VALUESET_BINDER.fromCodeString(bundleTypeValues[0]);
} else {
bundleType = BundleTypeEnum.SEARCHSET;
}
var responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(
theRequestDetails, myRestfulServer.getDefaultResponseEncoding());
var linkEncoding = theRequestDetails.getParameters().containsKey(Constants.PARAM_FORMAT)
&& responseEncoding != null ? responseEncoding.getEncoding() : null;
return (B) BundleProviderUtil.createBundleFromBundleProvider(myRestfulServer,
theRequestDetails, count, linkSelf, includes, theBundleProvider, start, bundleType,
linkEncoding, thePagingAction);
}
// TODO: The main use case for this is paging through Bundles, but I suppose that technically
// we ought to handle any old link. Maybe this is also an escape hatch for "custom non-FHIR
// repository action"?
@Override
public <B extends IBaseBundle> B link(Class<B> theBundleType, String theUrl,
Map<String, String> theHeaders) {
var details = startWith(myRequestDetails).addHeaders(theHeaders).create();
var urlParts = UrlUtil.parseUrl(theUrl);
details.setCompleteUrl(theUrl);
details.setParameters(UrlUtil.parseQueryStrings(urlParts.getParams()));
var pagingProvider = myRestfulServer.getPagingProvider();
if (pagingProvider == null) {
throw new InvalidRequestException(Msg.code(2312) + "This server does not support paging");
}
var thePagingAction = details.getParameters().get(Constants.PARAM_PAGINGACTION)[0];
IBundleProvider bundleProvider;
String pageId = null;
String[] pageIdParams = details.getParameters().get(Constants.PARAM_PAGEID);
if (pageIdParams != null && pageIdParams.length > 0 && isNotBlank(pageIdParams[0])) {
pageId = pageIdParams[0];
}
if (pageId != null) {
// This is a page request by Search ID and Page ID
bundleProvider = pagingProvider.retrieveResultList(details, thePagingAction, pageId);
validateHaveBundleProvider(thePagingAction, bundleProvider);
} else {
// This is a page request by Search ID and Offset
bundleProvider = pagingProvider.retrieveResultList(details, thePagingAction);
validateHaveBundleProvider(thePagingAction, bundleProvider);
}
return createBundle(details, bundleProvider, thePagingAction);
}
private void validateHaveBundleProvider(String thePagingAction,
IBundleProvider theBundleProvider) {
// Return an HTTP 410 if the search is not known
if (theBundleProvider == null) {
ourLog.info("Client requested unknown paging ID[{}]", thePagingAction);
String msg = fhirContext().getLocalizer().getMessage(PageMethodBinding.class,
"unknownSearchId", thePagingAction);
throw new ResourceGoneException(Msg.code(2313) + msg);
}
}
@Override
public <C extends IBaseConformance> C capabilities(Class<C> theCapabilityStatementType,
Map<String, String> theHeaders) {
var method = myRestfulServer.getServerConformanceMethod();
if (method == null) {
return null;
}
var details = startWith(myRequestDetails).addHeaders(theHeaders).create();
return (C) method.provideCapabilityStatement(myRestfulServer, details);
}
@Override
public <B extends IBaseBundle> B transaction(B theBundle, Map<String, String> theHeaders) {
var details = startWith(myRequestDetails).addHeaders(theHeaders).create();
return (B) myDaoRegistry.getSystemDao().transaction(details, theBundle);
}
@Override
public <R extends IBaseResource, P extends IBaseParameters> R invoke(String theName,
P theParameters, Class<R> theReturnType, Map<String, String> theHeaders) {
var details = startWith(myRequestDetails).addHeaders(theHeaders).setOperation(theName)
.setParameters(theParameters).create();
return invoke(details);
}
@Override
public <P extends IBaseParameters> MethodOutcome invoke(String theName, P theParameters,
Map<String, String> theHeaders) {
var details = startWith(myRequestDetails).addHeaders(theHeaders).setOperation(theName)
.setParameters(theParameters).create();
return invoke(details);
}
@Override
public <R extends IBaseResource, P extends IBaseParameters, T extends IBaseResource> R invoke(
Class<T> theResourceType, String theName, P theParameters, Class<R> theReturnType,
Map<String, String> theHeaders) {
var details = startWith(myRequestDetails).addHeaders(theHeaders).setOperation(theName)
.setResourceType(theResourceType.getSimpleName()).setParameters(theParameters).create();
return invoke(details);
}
@Override
public <P extends IBaseParameters, T extends IBaseResource> MethodOutcome invoke(
Class<T> theResourceType, String theName, P theParameters,
Map<String, String> theHeaders) {
var details = startWith(myRequestDetails).addHeaders(theHeaders).setOperation(theName)
.setResourceType(theResourceType.getSimpleName()).setParameters(theParameters).create();
return invoke(details);
}
@Override
public <R extends IBaseResource, P extends IBaseParameters, I extends IIdType> R invoke(I theId,
String theName, P theParameters, Class<R> theReturnType, Map<String, String> theHeaders) {
var details = startWith(myRequestDetails).addHeaders(theHeaders).setOperation(theName)
.setResourceType(theId.getResourceType()).setId(theId).setParameters(theParameters)
.create();
return invoke(details);
}
@Override
public <P extends IBaseParameters, I extends IIdType> MethodOutcome invoke(I theId,
String theName, P theParameters, Map<String, String> theHeaders) {
var details = startWith(myRequestDetails).addHeaders(theHeaders).setOperation(theName)
.setResourceType(theId.getResourceType()).setId(theId).setParameters(theParameters)
.create();
return invoke(details);
}
private void notImplemented() {
throw new NotImplementedOperationException(Msg.code(2314) + "history not yet implemented");
}
@Override
public <B extends IBaseBundle, P extends IBaseParameters> B history(P theParameters,
Class<B> theReturnBundleType, Map<String, String> theHeaders) {
notImplemented();
return null;
}
@Override
public <B extends IBaseBundle, P extends IBaseParameters, T extends IBaseResource> B history(
Class<T> theResourceType, P theParameters, Class<B> theReturnBundleType,
Map<String, String> theHeaders) {
notImplemented();
return null;
}
@Override
public <B extends IBaseBundle, P extends IBaseParameters, I extends IIdType> B history(I theId,
P theParameters, Class<B> theReturnBundleType, Map<String, String> theHeaders) {
notImplemented();
return null;
}
@Override
public FhirContext fhirContext() {
return myRestfulServer.getFhirContext();
}
protected <R extends Object> R invoke(RequestDetails theDetails) {
try {
return (R) myRestfulServer.determineResourceMethod(theDetails, null)
.invokeServer(myRestfulServer, theDetails);
} catch (IOException e) {
throw new RuntimeException(Msg.code(2315) + e);
}
}
}

View File

@ -0,0 +1,109 @@
package ca.uhn.fhir.cr.repo;
import java.util.HashMap;
import java.util.Map;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IIdType;
/*-
* #%L
* HAPI FHIR - Clinical Reasoning
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
/**
* This class produces partial clones of RequestDetails, the intent being to reuse the context of a
* RequestDetails object for reentrant calls. It retains header and tenancy information while
* scrapping everything else.
*/
class RequestDetailsCloner {
static DetailsBuilder startWith(RequestDetails theDetails) {
var newDetails = new SystemRequestDetails(theDetails);
newDetails.setRequestType(RequestTypeEnum.POST);
newDetails.setOperation(null);
newDetails.setResource(null);
newDetails.setParameters(new HashMap<>());
newDetails.setResourceName(null);
newDetails.setCompartmentName(null);
return new DetailsBuilder(newDetails);
}
static class DetailsBuilder {
private final SystemRequestDetails myDetails;
DetailsBuilder(SystemRequestDetails theDetails) {
myDetails = theDetails;
}
DetailsBuilder addHeaders(Map<String, String> theHeaders) {
if (theHeaders != null) {
for (var entry : theHeaders.entrySet()) {
myDetails.addHeader(entry.getKey(), entry.getValue());
}
}
return this;
}
DetailsBuilder setParameters(IBaseParameters theParameters) {
myDetails.setResource(theParameters);
return this;
}
DetailsBuilder setParameters(Map<String, String[]> theParameters) {
myDetails.setParameters(theParameters);
return this;
}
DetailsBuilder withRestOperationType(RequestTypeEnum theType) {
myDetails.setRequestType(theType);
return this;
}
DetailsBuilder setOperation(String theOperation) {
myDetails.setOperation(theOperation);
return this;
}
DetailsBuilder setResourceType(String theResourceName) {
myDetails.setResourceName(theResourceName);
return this;
}
DetailsBuilder setId(IIdType theId) {
myDetails.setId(theId);
return this;
}
SystemRequestDetails create() {
return myDetails;
}
}
}

View File

@ -0,0 +1,110 @@
package ca.uhn.fhir.cr.repo;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
/*-
* #%L
* HAPI FHIR - Clinical Reasoning
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.IQueryParameterAnd;
import ca.uhn.fhir.model.api.IQueryParameterOr;
import ca.uhn.fhir.model.api.IQueryParameterType;
/**
* The IGenericClient API represents searches with OrLists, while the FhirRepository API uses nested
* lists. This class (will eventually) convert between them
*/
public class SearchConverter {
// hardcoded list from FHIR specs: https://www.hl7.org/fhir/search.html
private final List<String> searchResultParameters = Arrays.asList("_sort", "_count", "_include",
"_revinclude", "_summary", "_total", "_elements", "_contained", "_containedType");
public final Map<String, List<IQueryParameterType>> separatedSearchParameters = new HashMap<>();
public final Map<String, List<IQueryParameterType>> separatedResultParameters = new HashMap<>();
public final SearchParameterMap searchParameterMap = new SearchParameterMap();
public final Map<String, String[]> resultParameters = new HashMap<>();
void convertParameters(Map<String, List<IQueryParameterType>> theParameters,
FhirContext theFhirContext) {
separateParameterTypes(theParameters);
convertToSearchParameterMap(separatedSearchParameters);
convertToStringMap(separatedResultParameters, theFhirContext);
}
public void convertToStringMap(@Nonnull Map<String, List<IQueryParameterType>> theParameters,
@Nonnull FhirContext theFhirContext) {
for (var entry : theParameters.entrySet()) {
String[] values = new String[entry.getValue().size()];
for (int i = 0; i < entry.getValue().size(); i++) {
values[i] = entry.getValue().get(i).getValueAsQueryToken(theFhirContext);
}
resultParameters.put(entry.getKey(), values);
}
}
public void convertToSearchParameterMap(Map<String, List<IQueryParameterType>> theSearchMap) {
if (theSearchMap == null) {
return;
}
for (var entry : theSearchMap.entrySet()) {
for (IQueryParameterType value : entry.getValue()) {
setParameterTypeValue(entry.getKey(), value);
}
}
}
public <T> void setParameterTypeValue(@Nonnull String theKey, @Nonnull T theParameterType) {
if (isOrList(theParameterType)) {
searchParameterMap.add(theKey, (IQueryParameterOr<?>) theParameterType);
} else if (isAndList(theParameterType)) {
searchParameterMap.add(theKey, (IQueryParameterAnd<?>) theParameterType);
} else {
searchParameterMap.add(theKey, (IQueryParameterType) theParameterType);
}
}
public void separateParameterTypes(
@Nonnull Map<String, List<IQueryParameterType>> theParameters) {
for (var entry : theParameters.entrySet()) {
if (isSearchResultParameter(entry.getKey())) {
separatedResultParameters.put(entry.getKey(), entry.getValue());
} else {
separatedSearchParameters.put(entry.getKey(), entry.getValue());
}
}
}
public boolean isSearchResultParameter(String theParameterName) {
return searchResultParameters.contains(theParameterName);
}
public <T> boolean isOrList(@Nonnull T theParameterType) {
return IQueryParameterOr.class.isAssignableFrom(theParameterType.getClass());
}
public <T> boolean isAndList(@Nonnull T theParameterType) {
return IQueryParameterAnd.class.isAssignableFrom(theParameterType.getClass());
}
}

View File

@ -3,8 +3,10 @@ package ca.uhn.fhir.cr;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.cr.config.CrR4Config;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.provider.r4.BaseResourceProviderR4Test;
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.server.RestfulServer;
import io.specto.hoverfly.junit.dsl.HoverflyDsl;
import io.specto.hoverfly.junit.dsl.StubServiceBuilder;
import io.specto.hoverfly.junit.rule.HoverflyRule;
@ -27,7 +29,7 @@ import static io.specto.hoverfly.junit.dsl.ResponseCreators.success;
@ContextConfiguration(classes = {TestCrConfig.class, CrR4Config.class})
public abstract class BaseCrR4Test extends BaseJpaR4Test implements IResourceLoader {
public abstract class BaseCrR4Test extends BaseResourceProviderR4Test implements IResourceLoader {
protected static final FhirContext ourFhirContext = FhirContext.forR4Cached();
private static final IParser ourParser = ourFhirContext.newJsonParser().setPrettyPrint(true);
private static final String TEST_ADDRESS = "test-address.com";

View File

@ -0,0 +1,165 @@
package ca.uhn.fhir.cr.r4;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Encounter;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Immunization;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import ca.uhn.fhir.cr.BaseCrR4Test;
import ca.uhn.fhir.cr.repo.HapiFhirRepository;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.NumberParam;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
public class HapiFhirRepositoryR4Test extends BaseCrR4Test {
private static final String MY_TEST_DATA =
"ca/uhn/fhir/cr/r4/immunization/Patients_Encounters_Immunizations_Practitioners.json";
private RequestDetails setupRequestDetails() {
var requestDetails = new ServletRequestDetails();
requestDetails.setServletRequest(new MockHttpServletRequest());
requestDetails.setServer(ourRestServer);
requestDetails.setFhirServerBase(ourServerBase);
return requestDetails;
}
@Test
void crudTest() {
var requestDetails = setupRequestDetails();
var repository = new HapiFhirRepository(myDaoRegistry, requestDetails, ourRestServer);
var result = repository
.create(new Patient().addName(new HumanName().setFamily("Test").addGiven("Name1")));
assertEquals(true, result.getCreated());
var patient = (Patient) result.getResource();
assertEquals(1, patient.getName().size());
assertEquals("Test", patient.getName().get(0).getFamily());
assertEquals(1, patient.getName().get(0).getGiven().size());
patient.getName().get(0).addGiven("Name2");
repository.update(patient);
var updatedPatient = repository.read(Patient.class, patient.getIdElement());
assertEquals(2, updatedPatient.getName().get(0).getGiven().size());
repository.delete(Patient.class, patient.getIdElement());
var ex = assertThrows(Exception.class,
() -> repository.read(Patient.class, new IdType(patient.getIdElement().getIdPart())));
assertTrue(ex.getMessage().contains("Resource was deleted"));
}
@Test
void canSearchMoreThan50Patients() {
loadBundle(MY_TEST_DATA);
var expectedPatientCount = 63;
ourPagingProvider.setMaximumPageSize(100);
var repository = new HapiFhirRepository(myDaoRegistry, setupRequestDetails(), ourRestServer);
// get all patient resources posted
var result = repository.search(Bundle.class, Patient.class, withCountParam(100));
assertEquals(expectedPatientCount, result.getTotal());
// count all resources in result
int counter = 0;
for (var e : result.getEntry()) {
counter++;
}
// verify all patient resources captured
assertEquals(expectedPatientCount, counter,
"Patient search results don't match available resources");
}
@Test
void canSearchWithPagination() {
loadBundle(MY_TEST_DATA);
var requestDetails = setupRequestDetails();
requestDetails.setCompleteUrl("http://localhost:44465/fhir/context/Patient?_count=20");
var repository = new HapiFhirRepository(myDaoRegistry, requestDetails, ourRestServer);
var result = repository.search(Bundle.class, Patient.class, withCountParam(20));
assertEquals(20, result.getEntry().size());
var next = result.getLink().get(1);
assertEquals("next", next.getRelation());
var nextUrl = next.getUrl();
var nextResult = repository.link(Bundle.class, nextUrl);
assertEquals(20, nextResult.getEntry().size());
assertEquals(false,
result.getEntry().stream().map(e -> e.getResource().getIdPart()).anyMatch(
i -> nextResult.getEntry().stream().map(e -> e.getResource().getIdPart())
.collect(Collectors.toList()).contains(i)));
}
@Test
void transactionReadsPatientResources() {
var expectedPatientCount = 63;
var theBundle = readResource(Bundle.class, MY_TEST_DATA);
ourPagingProvider.setMaximumPageSize(100);
var repository = new HapiFhirRepository(myDaoRegistry, setupRequestDetails(), ourRestServer);
repository.transaction(theBundle);
var result = repository.search(Bundle.class, Patient.class, withCountParam(100));
// count all resources in result
int counter = 0;
for (Object i : result.getEntry()) {
counter++;
}
// verify all patient resources captured
assertEquals(expectedPatientCount, counter,
"Patient search results don't match available resources");
}
@Test
void transactionReadsEncounterResources() {
var expectedEncounterCount = 652;
var theBundle = readResource(Bundle.class, MY_TEST_DATA);
ourPagingProvider.setMaximumPageSize(1000);
var repository = new HapiFhirRepository(myDaoRegistry, setupRequestDetails(), ourRestServer);
repository.transaction(theBundle);
var result = repository.search(Bundle.class, Encounter.class, withCountParam(1000));
// count all resources in result
int counter = 0;
for (Object i : result.getEntry()) {
counter++;
}
// verify all encounter resources captured
assertEquals(expectedEncounterCount, counter,
"Encounter search results don't match available resources");
}
@Test
void transactionReadsImmunizationResources() {
var expectedEncounterCount = 638;
var theBundle = readResource(Bundle.class, MY_TEST_DATA);
ourPagingProvider.setMaximumPageSize(1000);
var repository = new HapiFhirRepository(myDaoRegistry, setupRequestDetails(), ourRestServer);
repository.transaction(theBundle);
var result = repository.search(Bundle.class, Immunization.class, withCountParam(1000));
// count all resources in result
int counter = 0;
for (Object i : result.getEntry()) {
counter++;
}
// verify all immunization resources captured
assertEquals(expectedEncounterCount, counter,
"Immunization search results don't match available resources");
}
Map<String, List<IQueryParameterType>> withEmptySearchParams() {
return new HashMap<>();
}
Map<String, List<IQueryParameterType>> withCountParam(int theCount) {
var params = withEmptySearchParams();
params.put(Constants.PARAM_COUNT, Collections.singletonList(new NumberParam(theCount)));
return params;
}
}

View File

@ -0,0 +1,193 @@
package ca.uhn.fhir.cr.r4;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.cr.repo.SearchConverter;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.param.NumberAndListParam;
import ca.uhn.fhir.rest.param.NumberOrListParam;
import ca.uhn.fhir.rest.param.SpecialAndListParam;
import ca.uhn.fhir.rest.param.SpecialOrListParam;
import ca.uhn.fhir.rest.param.TokenAndListParam;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.UriAndListParam;
import ca.uhn.fhir.rest.param.UriOrListParam;
import ca.uhn.fhir.rest.param.UriParam;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class SearchConverterTest {
private SearchConverter myFixture;
@BeforeEach
public void setupFixture() {
myFixture = new SearchConverter();
}
@Test
void isSearchParameterShouldReturnTrue() {
boolean result = myFixture.isSearchResultParameter("_elements");
assertTrue(result);
}
@Test
void isSearchParameterShouldReturnFalse() {
boolean result = myFixture.isSearchResultParameter("_id");
assertFalse(result);
}
@Test
void isOrListShouldReturnTrue() {
boolean uriOrList = myFixture.isOrList(new UriOrListParam());
boolean numberOrList = myFixture.isOrList(new NumberOrListParam());
boolean specialOrList = myFixture.isOrList(new SpecialOrListParam());
boolean tokenOrList = myFixture.isOrList(new TokenOrListParam());
assertTrue(uriOrList);
assertTrue(numberOrList);
assertTrue(specialOrList);
assertTrue(tokenOrList);
}
@Test
void isAndListShouldReturnTrue() {
boolean uriAndList = myFixture.isAndList(new UriAndListParam());
boolean numberAndList = myFixture.isAndList(new NumberAndListParam());
boolean specialAndList = myFixture.isAndList(new SpecialAndListParam());
boolean tokenAndList = myFixture.isAndList(new TokenAndListParam());
assertTrue(uriAndList);
assertTrue(numberAndList);
assertTrue(specialAndList);
assertTrue(tokenAndList);
}
@Test
void isOrListShouldReturnFalse() {
boolean uriAndList = myFixture.isOrList(new UriAndListParam());
assertFalse(uriAndList);
}
@Test
void isAndListShouldReturnFalse() {
boolean uriAndList = myFixture.isAndList(new UriOrListParam());
assertFalse(uriAndList);
}
@Test
void setParameterTypeValueShouldSetWithOrValue() {
String theKey = "theOrKey";
UriOrListParam theValue = withUriOrListParam();
myFixture.setParameterTypeValue(theKey, theValue);
String result = myFixture.searchParameterMap.toNormalizedQueryString(withFhirContext());
String expected = "?theOrKey=theSecondValue,theValue";
assertEquals(expected, result);
}
@Test
void setParameterTypeValueShouldSetWithAndValue() {
String theKey = "theAndKey";
UriAndListParam theValue = withUriAndListParam();
myFixture.setParameterTypeValue(theKey, theValue);
String result = myFixture.searchParameterMap.toNormalizedQueryString(withFhirContext());
String expected =
"?theAndKey=theSecondValue,theValue&theAndKey=theSecondValueAgain,theValueAgain";
assertEquals(expected, result);
}
@Test
void setParameterTypeValueShouldSetWithBaseValue() {
String expected = "?theKey=theValue";
UriParam theValue = new UriParam("theValue");
String theKey = "theKey";
myFixture.setParameterTypeValue(theKey, theValue);
String result = myFixture.searchParameterMap.toNormalizedQueryString(withFhirContext());
assertEquals(expected, result);
}
@Test
void separateParameterTypesShouldSeparateSearchAndResultParams() {
myFixture.separateParameterTypes(withParamList());
assertEquals(2, myFixture.separatedSearchParameters.size());
assertEquals(3, myFixture.separatedResultParameters.size());
}
@Test
void convertToStringMapShouldConvert() {
Map<String, String[]> expected = withParamListAsStrings();
myFixture.convertToStringMap(withParamList(), withFhirContext());
Map<String, String[]> result = myFixture.resultParameters;
assertEquals(result.keySet(), expected.keySet());
assertTrue(result.entrySet().stream()
.allMatch(e -> Arrays.equals(e.getValue(), expected.get(e.getKey()))));
}
Map<String, List<IQueryParameterType>> withParamList() {
Map<String, List<IQueryParameterType>> paramList = new HashMap<>();
paramList.put("_id", withUriParam(1));
paramList.put("_elements", withUriParam(2));
paramList.put("_lastUpdated", withUriParam(1));
paramList.put("_total", withUriParam(1));
paramList.put("_count", withUriParam(3));
return paramList;
}
Map<String, String[]> withParamListAsStrings() {
Map<String, String[]> paramList = new HashMap<>();
paramList.put("_id", withStringParam(1));
paramList.put("_elements", withStringParam(2));
paramList.put("_lastUpdated", withStringParam(1));
paramList.put("_total", withStringParam(1));
paramList.put("_count", withStringParam(3));
return paramList;
}
List<IQueryParameterType> withUriParam(int theNumberOfParams) {
List<IQueryParameterType> paramList = new ArrayList<>();
for (int i = 0; i < theNumberOfParams; i++) {
paramList.add(new UriParam(Integer.toString(i)));
}
return paramList;
}
UriOrListParam withUriOrListParam() {
UriOrListParam orList = new UriOrListParam();
orList.add(new UriParam("theValue"));
orList.add(new UriParam("theSecondValue"));
return orList;
}
UriOrListParam withUriOrListParamSecond() {
UriOrListParam orList = new UriOrListParam();
orList.add(new UriParam("theValueAgain"));
orList.add(new UriParam("theSecondValueAgain"));
return orList;
}
UriAndListParam withUriAndListParam() {
UriAndListParam andList = new UriAndListParam();
andList.addAnd(withUriOrListParam());
andList.addAnd(withUriOrListParamSecond());
return andList;
}
String[] withStringParam(int theNumberOfParams) {
String[] paramList = new String[theNumberOfParams];
for (int i = 0; i < theNumberOfParams; i++) {
paramList[i] = Integer.toString(i);
}
return paramList;
}
FhirContext withFhirContext() {
return new FhirContext();
}
}

View File

@ -962,9 +962,7 @@
<elastic_apm_version>1.28.4</elastic_apm_version>
<!-- CQL Support -->
<cqframework.version>2.7.0-SNAPSHOT</cqframework.version>
<cql-engine.version>3.0.0-SNAPSHOT</cql-engine.version>
<cql-evaluator.version>3.0.0-SNAPSHOT</cql-evaluator.version>
<clinical-reasoning.version>3.0.0.PRE-1</clinical-reasoning.version>
<!-- Site properties -->
<fontawesomeVersion>5.4.1</fontawesomeVersion>