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:
parent
02556187a3
commit
182ac3b36c
|
@ -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."
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -201,7 +201,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer<Servlet
|
|||
/**
|
||||
* @since 5.5.0
|
||||
*/
|
||||
protected ConformanceMethodBinding getServerConformanceMethod() {
|
||||
public ConformanceMethodBinding getServerConformanceMethod() {
|
||||
return myServerConformanceMethod;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
4
pom.xml
4
pom.xml
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue