Merge branch 'master' into ja_20200206_multitenancy

This commit is contained in:
jamesagnew 2020-04-21 20:54:56 -04:00
commit 6226381595
25 changed files with 363 additions and 153 deletions

View File

@ -40,10 +40,8 @@
</p>
</th:block>
<p>
<b style="color: red;">
<span class="glyphicon glyphicon-warning-sign"/>
This is not a production server!
</b>
<b class="text-danger"><span class="glyphicon glyphicon-warning-sign"/></b>
<b><span class="text-danger">This is not a production server!</span></b>
Do not store any information here that contains personal health information
or any other confidential information. This server will be regularly purged
and reloaded with fixed test data.

View File

@ -40,10 +40,8 @@
</p>
</th:block>
<p>
<b style="color: red;">
<span class="glyphicon glyphicon-warning-sign"/>
This is not a production server!
</b>
<b class="text-danger"><span class="glyphicon glyphicon-warning-sign"/></b>
<b><span class="text-danger">This is not a production server!</span></b>
Do not store any information here that contains personal health information
or any other confidential information. This server will be regularly purged
and reloaded with fixed test data.

View File

@ -40,10 +40,8 @@
</p>
</th:block>
<p>
<b style="color: red;">
<span class="glyphicon glyphicon-warning-sign"/>
This is not a production server!
</b>
<b class="text-danger"><span class="glyphicon glyphicon-warning-sign"/></b>
<b><span class="text-danger">This is not a production server!</span></b>
Do not store any information here that contains personal health information
or any other confidential information. This server will be regularly purged
and reloaded with fixed test data.

View File

@ -21,9 +21,10 @@ package ca.uhn.fhir.rest.gclient;
*/
import ca.uhn.fhir.rest.api.DeleteCascadeModeEnum;
import ca.uhn.fhir.rest.api.MethodOutcome;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
public interface IDeleteTyped extends IClientExecutable<IDeleteTyped, IBaseOperationOutcome> {
public interface IDeleteTyped extends IClientExecutable<IDeleteTyped, MethodOutcome> {
/**
* Delete cascade mode - Note that this is a HAPI FHIR specific feature and is not supported on all servers.

View File

@ -72,6 +72,7 @@ public interface IQuery<Y> extends IBaseQuery<IQuery<Y>>, IClientExecutable<IQue
* on a single page.
*
* @deprecated This parameter is badly named, since FHIR calls this parameter "_count" and not "_limit". Use {@link #count(int)} instead (it also sets the _count parameter)
* @see #count(int)
*/
@Deprecated
IQuery<Y> limitTo(int theLimitTo);

View File

@ -139,6 +139,8 @@ ca.uhn.fhir.jpa.term.BaseTermReadSvcImpl.expansionTooLarge=Expansion of ValueSet
ca.uhn.fhir.jpa.util.jsonpatch.JsonPatchUtils.failedToApplyPatch=Failed to apply JSON patch to {0}: {1}
ca.uhn.fhir.jpa.graphql.JpaStorageServices.invalidGraphqlArgument=Unknown GraphQL argument "{0}". Value GraphQL argument for this type are: {1}
ca.uhn.fhir.jpa.partition.RequestPartitionHelperService.blacklistedResourceTypeForPartitioning=Resource type {0} can not be partitioned
ca.uhn.fhir.jpa.partition.RequestPartitionHelperService.unknownPartitionId=Unknown partition ID: {0}
ca.uhn.fhir.jpa.partition.RequestPartitionHelperService.unknownPartitionName=Unknown partition name: {0}

View File

@ -604,7 +604,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
private class DeleteInternal extends BaseSearch<IDeleteTyped, IDeleteWithQueryTyped, IBaseOperationOutcome> implements IDelete, IDeleteTyped, IDeleteWithQuery, IDeleteWithQueryTyped {
private class DeleteInternal extends BaseSearch<IDeleteTyped, IDeleteWithQueryTyped, MethodOutcome> implements IDelete, IDeleteTyped, IDeleteWithQuery, IDeleteWithQueryTyped {
private boolean myConditional;
private IIdType myId;
@ -613,7 +613,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
private DeleteCascadeModeEnum myCascadeMode;
@Override
public IBaseOperationOutcome execute() {
public MethodOutcome execute() {
Map<String, List<String>> additionalParams = new HashMap<>();
if (myCascadeMode != null) {
@ -635,7 +635,8 @@ public class GenericClient extends BaseClient implements IGenericClient {
} else {
invocation = DeleteMethodBinding.createDeleteInvocation(getFhirContext(), mySearchUrl, getParamMap());
}
OperationOutcomeResponseHandler binding = new OperationOutcomeResponseHandler();
OutcomeResponseHandler binding = new OutcomeResponseHandler();
return invoke(additionalParams, binding, invocation);
}
@ -1389,30 +1390,6 @@ public class GenericClient extends BaseClient implements IGenericClient {
}
}
private final class OperationOutcomeResponseHandler implements IClientResponseHandler<IBaseOperationOutcome> {
@Override
public IBaseOperationOutcome invokeClient(String theResponseMimeType, InputStream theResponseInputStream, int theResponseStatusCode, Map<String, List<String>> theHeaders)
throws BaseServerResponseException {
EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType);
if (respType == null) {
return null;
}
IParser parser = respType.newParser(myContext);
IBaseOperationOutcome retVal;
try {
// TODO: handle if something else than OO comes back
retVal = (IBaseOperationOutcome) parser.parseResource(theResponseInputStream);
} catch (DataFormatException e) {
ourLog.warn("Failed to parse OperationOutcome response", e);
return null;
}
MethodUtil.parseClientRequestResourceHeaders(null, theHeaders, retVal);
return retVal;
}
}
private final class OutcomeResponseHandler implements IClientResponseHandler<MethodOutcome> {
private PreferReturnEnum myPrefer;

View File

@ -236,21 +236,27 @@ public class GenericClientExample {
// START SNIPPET: conformance
// Retrieve the server's conformance statement and print its
// description
CapabilityStatement conf = client.capabilities().ofType(CapabilityStatement.class).execute();
CapabilityStatement conf = client
.capabilities()
.ofType(CapabilityStatement.class)
.execute();
System.out.println(conf.getDescriptionElement().getValue());
// END SNIPPET: conformance
}
{
// START SNIPPET: delete
IBaseOperationOutcome resp = client.delete().resourceById(new IdType("Patient", "1234")).execute();
MethodOutcome response = client
.delete()
.resourceById(new IdType("Patient", "1234"))
.execute();
// outcome may be null if the server didn't return one
if (resp != null) {
OperationOutcome outcome = (OperationOutcome) resp;
System.out.println(outcome.getIssueFirstRep().getDetails().getCodingFirstRep().getCode());
}
// END SNIPPET: delete
}
OperationOutcome outcome = (OperationOutcome) response.getOperationOutcome();
if (outcome != null) {
System.out.println(outcome.getIssueFirstRep().getDetails().getCodingFirstRep().getCode());
}
// END SNIPPET: delete
}
{
// START SNIPPET: deleteConditional
client.delete()
@ -356,7 +362,8 @@ public class GenericClientExample {
.revInclude(Provenance.INCLUDE_TARGET)
.lastUpdated(new DateRangeParam("2011-01-01", null))
.sort().ascending(Patient.BIRTHDATE)
.sort().descending(Patient.NAME).limitTo(123)
.sort().descending(Patient.NAME)
.count(123)
.returnBundle(Bundle.class)
.execute();
// END SNIPPET: searchAdv

View File

@ -0,0 +1,5 @@
---
type: fix
issue: 1791
title: The GraphQL Expression parser sometimes fails and reports unhelpful error messages when using search arguments.
Thanks to Ibrohim Kholilul Islam for the pull request!

View File

@ -0,0 +1,6 @@
---
type: fix
issue: 1806
title: The JPA server ElasticSearch provider failed to initialize if username/password credentials were not
explicitly provided, meaning it could not run on AWS-supplied ElasticSearch. Thanks to Maciej Kucharek for
the pull request!

View File

@ -0,0 +1,5 @@
---
type: fix
issue: 1810
title: The text styling on the Testpage Overlay homepage has been improved to use native
Bootstrap warning colours. Thanks to Joel Schneider for the pull request!

View File

@ -36,6 +36,23 @@
These classes have not changed in terms of functionality, but existing projects may need to adjust some
package import statements.
"
- item:
issue: "1804"
type: "change"
title: "**Breaking Change**:
The Generic/Fluent **delete()** operation now returns a [MethodOutcome](/apidocs/hapi-fhir-base/ca/uhn/fhir/rest/api/MethodOutcome.html)
object instead of an OperationOutcome. The OperationOutcomoe is still available direcly by querying
the MethodOutcome object, but this change makes the delete() method more consistent with
other similar methods in the API.
"
- item:
type: "change"
title: "**Breaking Change**:
Some R4 and R5 structure fields containing a `code` value with a **Required (closed) binding**
did not use the java Enum type that was generated for the given field. These have been changed
to use the Enum values where possible. This change does not remove any functionality from the model
but may require a small amount of re-coding to deal with new setter/getter types on a few fields.
"
- item:
issue: "1807"
type: "change"

View File

@ -168,7 +168,7 @@ public class AbstractJaxRsResourceProviderDstu3Test {
@Test
public void testDeletePatient() {
when(mock.delete(idCaptor.capture(), conditionalCaptor.capture())).thenReturn(new MethodOutcome());
final IBaseOperationOutcome results = client.delete().resourceById("Patient", "1").execute();
final IBaseOperationOutcome results = client.delete().resourceById("Patient", "1").execute().getOperationOutcome();
assertEquals("1", idCaptor.getValue().getIdPart());
}

View File

@ -164,7 +164,7 @@ public class AbstractJaxRsResourceProviderTest {
@Test
public void testDeletePatient() {
when(mock.delete(idCaptor.capture(), conditionalCaptor.capture())).thenReturn(new MethodOutcome());
final IBaseOperationOutcome results = client.delete().resourceById("Patient", "1").execute();
final IBaseOperationOutcome results = client.delete().resourceById("Patient", "1").execute().getOperationOutcome();
assertEquals("1", idCaptor.getValue().getIdPart());
}

View File

@ -45,21 +45,42 @@ import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.utilities.graphql.Argument;
import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
import org.hl7.fhir.utilities.graphql.Value;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import static ca.uhn.fhir.rest.api.Constants.PARAM_FILTER;
public class JpaStorageServices extends BaseHapiFhirDao<IBaseResource> implements IGraphQLStorageServices {
private static final int MAX_SEARCH_SIZE = 500;
private static final Logger ourLog = LoggerFactory.getLogger(JpaStorageServices.class);
private IFhirResourceDao<? extends IBaseResource> getDao(String theResourceType) {
RuntimeResourceDefinition typeDef = getContext().getResourceDefinition(theResourceType);
return myDaoRegistry.getResourceDaoOrNull(typeDef.getImplementingClass());
}
private String graphqlArgumentToSearchParam(String name) {
if (name.startsWith("_")) {
return name;
} else {
return name.replaceAll("_", "-");
}
}
private String searchParamToGraphqlArgument(String name) {
return name.replaceAll("-", "_");
}
@Transactional(propagation = Propagation.NEVER)
@Override
public void listResources(Object theAppInfo, String theType, List<Argument> theSearchParams, List<IBaseResource> theMatches) throws FHIRException {
@ -70,9 +91,25 @@ public class JpaStorageServices extends BaseHapiFhirDao<IBaseResource> implement
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronousUpTo(MAX_SEARCH_SIZE);
Map<String, RuntimeSearchParam> searchParams = mySearchParamRegistry.getActiveSearchParams(typeDef.getName());
for (Argument nextArgument : theSearchParams) {
RuntimeSearchParam searchParam = mySearchParamRegistry.getSearchParamByName(typeDef, nextArgument.getName());
if (nextArgument.getName().equals(PARAM_FILTER)) {
String value = nextArgument.getValues().get(0).getValue();
params.add(PARAM_FILTER, new StringParam(value));
continue;
}
String searchParamName = graphqlArgumentToSearchParam(nextArgument.getName());
RuntimeSearchParam searchParam = searchParams.get(searchParamName);
if (searchParam == null) {
Set<String> graphqlArguments = searchParams.keySet().stream()
.map(this::searchParamToGraphqlArgument)
.collect(Collectors.toSet());
String msg = getContext().getLocalizer().getMessageSanitized(JpaStorageServices.class, "invalidGraphqlArgument", nextArgument.getName(), new TreeSet<>(graphqlArguments));
throw new InvalidRequestException(msg);
}
for (Value nextValue : nextArgument.getValues()) {
String value = nextValue.getValue();
@ -108,7 +145,7 @@ public class JpaStorageServices extends BaseHapiFhirDao<IBaseResource> implement
break;
}
params.add(nextArgument.getName(), param);
params.add(searchParamName, param);
}
}

View File

@ -34,8 +34,10 @@ import ca.uhn.fhir.jpa.delete.DeleteConflictOutcome;
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.DeleteCascadeModeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.ResponseDetails;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.OperationOutcomeUtil;
import org.apache.commons.lang3.Validate;
@ -45,6 +47,9 @@ import org.hl7.fhir.r4.model.OperationOutcome;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.validation.constraints.Null;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
@ -92,7 +97,12 @@ public class CascadingDeleteInterceptor {
public DeleteConflictOutcome handleDeleteConflicts(DeleteConflictList theConflictList, RequestDetails theRequest) {
ourLog.debug("Have delete conflicts: {}", theConflictList);
if (!shouldCascade(theRequest)) {
if (shouldCascade(theRequest) == DeleteCascadeModeEnum.NONE) {
// Add a message to the response
String message = theRequest.getFhirContext().getLocalizer().getMessage(CascadingDeleteInterceptor.class, "noParam");
theRequest.getUserData().put(CASCADED_DELETES_FAILED_KEY, message);
return null;
}
@ -180,28 +190,12 @@ public class CascadingDeleteInterceptor {
/**
* Subclasses may override
*
* @param theRequest The REST request
* @param theRequest The REST request (may be null)
* @return Returns true if cascading delete should be allowed
*/
@SuppressWarnings("WeakerAccess")
protected boolean shouldCascade(RequestDetails theRequest) {
if (theRequest != null) {
String[] cascadeParameters = theRequest.getParameters().get(Constants.PARAMETER_CASCADE_DELETE);
if (cascadeParameters != null && Arrays.asList(cascadeParameters).contains(Constants.CASCADE_DELETE)) {
return true;
}
String cascadeHeader = theRequest.getHeader(Constants.HEADER_CASCADE);
if (Constants.CASCADE_DELETE.equals(cascadeHeader)) {
return true;
}
// Add a message to the response
String message = theRequest.getFhirContext().getLocalizer().getMessage(CascadingDeleteInterceptor.class, "noParam");
theRequest.getUserData().put(CASCADED_DELETES_FAILED_KEY, message);
}
return false;
@Nonnull
protected DeleteCascadeModeEnum shouldCascade(@Nullable RequestDetails theRequest) {
return RestfulServerUtils.extractDeleteCascadeParameter(theRequest);
}

View File

@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.search.elastic;
* #L%
*/
import org.apache.commons.lang3.StringUtils;
import org.hibernate.search.cfg.Environment;
import org.hibernate.search.elasticsearch.cfg.ElasticsearchEnvironment;
import org.hibernate.search.elasticsearch.cfg.ElasticsearchIndexStatus;
@ -63,8 +64,12 @@ public class ElasticsearchHibernatePropertiesBuilder {
theProperties.put("hibernate.search." + ElasticsearchEnvironment.ANALYSIS_DEFINITION_PROVIDER, ElasticsearchMappingProvider.class.getName());
theProperties.put("hibernate.search.default.elasticsearch.host", myRestUrl);
theProperties.put("hibernate.search.default.elasticsearch.username", myUsername);
theProperties.put("hibernate.search.default.elasticsearch.password", myPassword);
if (StringUtils.isNotBlank(myUsername)) {
theProperties.put("hibernate.search.default.elasticsearch.username", myUsername);
}
if (StringUtils.isNotBlank(myPassword)) {
theProperties.put("hibernate.search.default.elasticsearch.password", myPassword);
}
theProperties.put("hibernate.search.default." + ElasticsearchEnvironment.INDEX_SCHEMA_MANAGEMENT_STRATEGY, myIndexSchemaManagementStrategy.getExternalName());
theProperties.put("hibernate.search.default." + ElasticsearchEnvironment.INDEX_MANAGEMENT_WAIT_TIMEOUT, Long.toString(myIndexManagementWaitTimeoutMillis));

View File

@ -0,0 +1,88 @@
package ca.uhn.fhir.jpa.graphql;
import ca.uhn.fhir.jpa.config.TestR4Config;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Appointment;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.utilities.graphql.Argument;
import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
import org.hl7.fhir.utilities.graphql.StringValue;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ContextConfiguration(classes = {TestR4Config.class})
@RunWith(SpringJUnit4ClassRunner.class)
@DirtiesContext
public class JpaStorageServicesTest extends BaseJpaR4Test {
@After
public void after() {
myDaoConfig.setFilterParameterEnabled(new DaoConfig().isFilterParameterEnabled());
}
@Before
public void before() {
myDaoConfig.setFilterParameterEnabled(true);
}
@Autowired
private IGraphQLStorageServices mySvc;
private String createSomeAppointment() {
CodeableConcept someCodeableConcept = new CodeableConcept(new Coding("TEST_SYSTEM", "TEST_CODE", "TEST_DISPLAY"));
Appointment someAppointment = new Appointment();
someAppointment.setAppointmentType(someCodeableConcept);
return myAppointmentDao.create(someAppointment).getId().getIdPart();
}
@Test
public void testListResourcesGraphqlArgumentConversion() {
String appointmentId = createSomeAppointment();
Argument argument = new Argument("appointment_type", new StringValue("TEST_CODE"));
List<IBaseResource> result = new ArrayList<>();
mySvc.listResources(mySrd, "Appointment", Collections.singletonList(argument), result);
Assert.assertFalse(result.isEmpty());
Assert.assertTrue(result.stream().anyMatch((it) -> it.getIdElement().getIdPart().equals(appointmentId)));
}
@Test
public void testListResourceGraphqlFilterArgument() {
String appointmentId = createSomeAppointment();
Argument argument = new Argument("_filter", new StringValue("appointment-type eq TEST_CODE"));
List<IBaseResource> result = new ArrayList<>();
mySvc.listResources(mySrd, "Appointment", Collections.singletonList(argument), result);
Assert.assertFalse(result.isEmpty());
Assert.assertTrue(result.stream().anyMatch((it) -> it.getIdElement().getIdPart().equals(appointmentId)));
}
@Test(expected = InvalidRequestException.class)
public void testListResourceGraphqlInvalidException() {
Argument argument = new Argument("test", new StringValue("some test value"));
List<IBaseResource> result = new ArrayList<>();
mySvc.listResources(mySrd, "Appointment", Collections.singletonList(argument), result);
}
}

View File

@ -814,15 +814,13 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
myDaoConfig.setAllowMultipleDelete(true);
//@formatter:off
IBaseOperationOutcome response = ourClient
MethodOutcome response = ourClient
.delete()
.resourceConditionalByType(Patient.class)
.where(Patient.IDENTIFIER.exactly().code(methodName))
.execute();
//@formatter:on
String encoded = myFhirCtx.newXmlParser().encodeResourceToString(response);
String encoded = myFhirCtx.newXmlParser().encodeResourceToString(response.getOperationOutcome());
ourLog.info(encoded);
assertThat(encoded, containsString(
"<issue><severity value=\"information\"/><code value=\"informational\"/><diagnostics value=\"Successfully deleted 2 resource(s) in "));
@ -1028,8 +1026,8 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
p.addName().setFamily("FAM");
IIdType id = ourClient.create().resource(p).execute().getId().toUnqualifiedVersionless();
IBaseOperationOutcome resp = ourClient.delete().resourceById(id).execute();
OperationOutcome oo = (OperationOutcome) resp;
MethodOutcome resp = ourClient.delete().resourceById(id).execute();
OperationOutcome oo = (OperationOutcome) resp.getOperationOutcome();
assertThat(oo.getIssueFirstRep().getDiagnostics(), startsWith("Successfully deleted 1 resource(s) in "));
}

View File

@ -1257,15 +1257,13 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
myDaoConfig.setAllowMultipleDelete(true);
//@formatter:off
IBaseOperationOutcome response = ourClient
MethodOutcome response = ourClient
.delete()
.resourceConditionalByType(Patient.class)
.where(Patient.IDENTIFIER.exactly().code(methodName))
.execute();
//@formatter:on
String encoded = myFhirCtx.newXmlParser().encodeResourceToString(response);
String encoded = myFhirCtx.newXmlParser().encodeResourceToString(response.getOperationOutcome());
ourLog.info(encoded);
assertThat(encoded, containsString(
"<issue><severity value=\"information\"/><code value=\"informational\"/><diagnostics value=\"Successfully deleted 2 resource(s) in "));
@ -1478,8 +1476,8 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
p.addName().setFamily("FAM");
IIdType id = ourClient.create().resource(p).execute().getId().toUnqualifiedVersionless();
IBaseOperationOutcome resp = ourClient.delete().resourceById(id).execute();
OperationOutcome oo = (OperationOutcome) resp;
MethodOutcome resp = ourClient.delete().resourceById(id).execute();
OperationOutcome oo = (OperationOutcome) resp.getOperationOutcome();
assertThat(oo.getIssueFirstRep().getDiagnostics(), startsWith("Successfully deleted 1 resource(s) in "));
}

View File

@ -40,10 +40,8 @@
</p>
</th:block>
<p>
<b style="color: red;">
<span class="glyphicon glyphicon-warning-sign"/>
This is not a production server!
</b>
<b class="text-danger"><span class="glyphicon glyphicon-warning-sign"/></b>
<b><span class="text-danger">This is not a production server!</span></b>
Do not store any information here that contains personal health information
or any other confidential information. This server will be regularly purged
and reloaded with fixed test data.

View File

@ -31,6 +31,7 @@ import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.DeleteCascadeModeEnum;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.PreferHeader;
import ca.uhn.fhir.rest.api.PreferReturnEnum;
@ -78,6 +79,7 @@ public class RestfulServerUtils {
private static final HashSet<String> TEXT_ENCODE_ELEMENTS = new HashSet<>(Arrays.asList("*.text", "*.id", "*.meta", "*.(mandatory)"));
private static Map<FhirVersionEnum, FhirContext> myFhirContextMap = Collections.synchronizedMap(new HashMap<>());
private static EnumSet<RestOperationTypeEnum> ourOperationsWhichAllowPreferHeader = EnumSet.of(RestOperationTypeEnum.CREATE, RestOperationTypeEnum.UPDATE, RestOperationTypeEnum.PATCH);
private enum NarrativeModeEnum {
NORMAL, ONLY, SUPPRESS;
@ -696,59 +698,55 @@ public class RestfulServerUtils {
return retVal;
}
private static EnumSet<RestOperationTypeEnum> ourOperationsWhichAllowPreferHeader = EnumSet.of(RestOperationTypeEnum.CREATE, RestOperationTypeEnum.UPDATE, RestOperationTypeEnum.PATCH);
public static boolean respectPreferHeader(RestOperationTypeEnum theRestOperationType) {
return ourOperationsWhichAllowPreferHeader.contains(theRestOperationType);
}
@Nonnull
public static PreferHeader parsePreferHeader(IRestfulServer<?> theServer, String theValue) {
PreferHeader retVal = new PreferHeader();
if (isNotBlank(theValue)) {
StringTokenizer tok = new StringTokenizer(theValue, ";");
while (tok.hasMoreTokens()) {
String next = trim(tok.nextToken());
int eqIndex = next.indexOf('=');
String key;
String value;
if (eqIndex == -1 || eqIndex >= next.length() - 2) {
key = next;
value = "";
} else {
key = next.substring(0, eqIndex).trim();
value = next.substring(eqIndex + 1).trim();
}
if (key.equals(Constants.HEADER_PREFER_RETURN)) {
if (value.length() < 2) {
continue;
}
if ('"' == value.charAt(0) && '"' == value.charAt(value.length() - 1)) {
value = value.substring(1, value.length() - 1);
}
retVal.setReturn(PreferReturnEnum.fromHeaderValue(value));
} else if (key.equals(Constants.HEADER_PREFER_RESPOND_ASYNC)) {
retVal.setRespondAsync(true);
}
}
}
public static PreferHeader parsePreferHeader(IRestfulServer<?> theServer, String theValue) {
PreferHeader retVal = new PreferHeader();
if (isNotBlank(theValue)) {
StringTokenizer tok = new StringTokenizer(theValue, ";");
while (tok.hasMoreTokens()) {
String next = trim(tok.nextToken());
int eqIndex = next.indexOf('=');
String key;
String value;
if (eqIndex == -1 || eqIndex >= next.length() - 2) {
key = next;
value = "";
} else {
key = next.substring(0, eqIndex).trim();
value = next.substring(eqIndex + 1).trim();
}
if (key.equals(Constants.HEADER_PREFER_RETURN)) {
if (value.length() < 2) {
continue;
}
if ('"' == value.charAt(0) && '"' == value.charAt(value.length() - 1)) {
value = value.substring(1, value.length() - 1);
}
retVal.setReturn(PreferReturnEnum.fromHeaderValue(value));
} else if (key.equals(Constants.HEADER_PREFER_RESPOND_ASYNC)) {
retVal.setRespondAsync(true);
}
}
}
if (retVal.getReturn() == null && theServer != null && theServer.getDefaultPreferReturn() != null) {
retVal.setReturn(theServer.getDefaultPreferReturn());
}
return retVal;
}
}
public static boolean prettyPrintResponse(IRestfulServerDefaults theServer, RequestDetails theRequest) {
@ -772,12 +770,12 @@ public class RestfulServerUtils {
}
public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource, Set<SummaryEnum> theSummaryMode, int stausCode, boolean theAddContentLocationHeader,
boolean respondGzip, RequestDetails theRequestDetails) throws IOException {
boolean respondGzip, RequestDetails theRequestDetails) throws IOException {
return streamResponseAsResource(theServer, theResource, theSummaryMode, stausCode, null, theAddContentLocationHeader, respondGzip, theRequestDetails, null, null);
}
public static Object streamResponseAsResource(IRestfulServerDefaults theServer, IBaseResource theResource, Set<SummaryEnum> theSummaryMode, int theStatusCode, String theStatusMessage,
boolean theAddContentLocationHeader, boolean respondGzip, RequestDetails theRequestDetails, IIdType theOperationResourceId, IPrimitiveType<Date> theOperationResourceLastUpdated)
boolean theAddContentLocationHeader, boolean respondGzip, RequestDetails theRequestDetails, IIdType theOperationResourceId, IPrimitiveType<Date> theOperationResourceLastUpdated)
throws IOException {
IRestfulResponse response = theRequestDetails.getResponse();
@ -954,5 +952,22 @@ public class RestfulServerUtils {
}
/**
* @since 5.0.0
*/
public static DeleteCascadeModeEnum extractDeleteCascadeParameter(RequestDetails theRequest) {
if (theRequest != null) {
String[] cascadeParameters = theRequest.getParameters().get(Constants.PARAMETER_CASCADE_DELETE);
if (cascadeParameters != null && Arrays.asList(cascadeParameters).contains(Constants.CASCADE_DELETE)) {
return DeleteCascadeModeEnum.DELETE;
}
String cascadeHeader = theRequest.getHeader(Constants.HEADER_CASCADE);
if (Constants.CASCADE_DELETE.equals(cascadeHeader)) {
return DeleteCascadeModeEnum.DELETE;
}
}
return DeleteCascadeModeEnum.NONE;
}
}

View File

@ -377,7 +377,7 @@ public class GenericClientR4Test extends BaseGenericClientR4Test {
});
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
IBaseOperationOutcome outcome;
MethodOutcome outcome;
// Regular delete
outcome = client
@ -1629,7 +1629,6 @@ public class GenericClientR4Test extends BaseGenericClientR4Test {
idx++;
}
@SuppressWarnings("deprecation")
@Test
public void testSearchByQuantity() throws Exception {
ArgumentCaptor<HttpUriRequest> capt = prepareClientForSearchResponse();

View File

@ -5,6 +5,7 @@ import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException;
import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException;
import ca.uhn.fhir.rest.client.impl.BaseClient;
import ca.uhn.fhir.rest.client.impl.GenericClient;
@ -386,23 +387,81 @@ public class GenericClientTest {
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
OperationOutcome outcome = (OperationOutcome) client.delete().resourceById("Patient", "123")
.withAdditionalHeader("myHeaderName", "myHeaderValue").execute();
MethodOutcome outcome = client
.delete()
.resourceById("Patient", "123")
.withAdditionalHeader("myHeaderName", "myHeaderValue")
.execute();
oo = (OperationOutcome) outcome.getOperationOutcome();
assertEquals("http://example.com/fhir/Patient/123", capt.getValue().getURI().toString());
assertEquals("DELETE", capt.getValue().getMethod());
Assert.assertEquals("testDelete01", outcome.getIssueFirstRep().getLocation().get(0).getValue());
Assert.assertEquals("testDelete01", oo.getIssueFirstRep().getLocation().get(0).getValue());
assertEquals("myHeaderValue", capt.getValue().getFirstHeader("myHeaderName").getValue());
}
@Test
public void testDeleteInvalidResponse() throws Exception {
OperationOutcome oo = new OperationOutcome();
oo.addIssue().addLocation("testDelete01");
String ooStr = ourCtx.newXmlParser().encodeResourceToString(oo);
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK"));
when(myHttpResponse.getAllHeaders()).thenReturn(new Header[]{new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22")});
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader("LKJHLKJGLKJKLL"), StandardCharsets.UTF_8));
outcome = (OperationOutcome) client.delete().resourceById(new IdType("Location", "123", "456")).prettyPrint().encodedJson().execute();
assertEquals("http://example.com/fhir/Location/123?_pretty=true", capt.getAllValues().get(1).getURI().toString());
assertEquals("DELETE", capt.getValue().getMethod());
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
Assert.assertEquals(null, outcome);
// Try with invalid response
try {
client
.delete()
.resourceById(new IdType("Location", "123", "456"))
.prettyPrint()
.encodedJson()
.execute();
} catch (FhirClientConnectionException e) {
assertEquals(0, e.getStatusCode());
assertThat(e.getMessage(), containsString("Failed to parse response from server when performing DELETE to URL"));
}
}
@Test
public void testDeleteNoResponse() throws Exception {
OperationOutcome oo = new OperationOutcome();
oo.addIssue().addLocation("testDelete01");
String ooStr = ourCtx.newXmlParser().encodeResourceToString(oo);
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK"));
when(myHttpResponse.getAllHeaders()).thenReturn(new Header[]{new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22")});
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(ooStr), StandardCharsets.UTF_8));
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
MethodOutcome outcome = client
.delete()
.resourceById("Patient", "123")
.withAdditionalHeader("myHeaderName", "myHeaderValue")
.execute();
oo = (OperationOutcome) outcome.getOperationOutcome();
assertEquals("http://example.com/fhir/Patient/123", capt.getValue().getURI().toString());
assertEquals("DELETE", capt.getValue().getMethod());
Assert.assertEquals("testDelete01", oo.getIssueFirstRep().getLocation().get(0).getValue());
assertEquals("myHeaderValue", capt.getValue().getFirstHeader("myHeaderName").getValue());
}
@Test
public void testHistory() throws Exception {
@ -413,12 +472,8 @@ public class GenericClientTest {
when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse);
when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer<InputStream>() {
@Override
public InputStream answer(InvocationOnMock theInvocation) throws Throwable {
return new ReaderInputStream(new StringReader(msg), StandardCharsets.UTF_8);
}
});
when(myHttpResponse.getEntity().getContent()).thenAnswer(t ->
new ReaderInputStream(new StringReader(msg), StandardCharsets.UTF_8));
IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir");
@ -428,7 +483,7 @@ public class GenericClientTest {
response = client
.history()
.onServer()
.andReturnBundle(Bundle.class)
.returnBundle(Bundle.class)
.withAdditionalHeader("myHeaderName", "myHeaderValue")
.execute();
assertEquals("http://example.com/fhir/_history", capt.getAllValues().get(idx).getURI().toString());
@ -439,7 +494,7 @@ public class GenericClientTest {
response = client
.history()
.onType(Patient.class)
.andReturnBundle(Bundle.class)
.returnBundle(Bundle.class)
.withAdditionalHeader("myHeaderName", "myHeaderValue1")
.withAdditionalHeader("myHeaderName", "myHeaderValue2")
.execute();

View File

@ -603,6 +603,14 @@
<id>dgileadi</id>
<name>David Gileadi</name>
</developer>
<developer>
<id>ibrohimislam</id>
<name>Ibrohim Kholilul Islam</name>
</developer>
<developer>
<id>mkucharek</id>
<name>Maciej Kucharek</name>
</developer>
</developers>
<licenses>