diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/FhirValidator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/FhirValidator.java index 8bf391e7fa4..b22b8eeefdc 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/FhirValidator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/FhirValidator.java @@ -167,7 +167,7 @@ public class FhirValidator { */ public synchronized FhirValidator registerValidatorModule(IValidatorModule theValidator) { Validate.notNull(theValidator, "theValidator must not be null"); - ArrayList newValidators = new ArrayList(myValidators.size() + 1); + ArrayList newValidators = new ArrayList<>(myValidators.size() + 1); newValidators.addAll(myValidators); newValidators.add(theValidator); diff --git a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/OperationOutcome.html b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/OperationOutcome.html index a84249a298f..04e92f945bb 100644 --- a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/OperationOutcome.html +++ b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/OperationOutcome.html @@ -11,7 +11,7 @@ -

+				
 			
 		
 	
diff --git a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/FhirTesterConfig.java b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/FhirTesterConfig.java
index c168b36b7e2..e1aaba9b1d7 100644
--- a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/FhirTesterConfig.java
+++ b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/FhirTesterConfig.java
@@ -21,6 +21,7 @@ package ca.uhn.hapi.fhir.docs;
  */
 
 import ca.uhn.fhir.context.FhirVersionEnum;
+import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
 import ca.uhn.fhir.to.FhirTesterMvcConfig;
 import ca.uhn.fhir.to.TesterConfig;
 import org.springframework.context.annotation.Bean;
@@ -60,15 +61,21 @@ public class FhirTesterConfig {
 		retVal
 			.addServer()
 				.withId("home")
-				.withFhirVersion(FhirVersionEnum.DSTU2)
+				.withFhirVersion(FhirVersionEnum.R4)
 				.withBaseUrl("${serverBase}/fhir")
 				.withName("Local Tester")
+				// Add a $diff button on search result rows where version > 1
+				.withSearchResultRowOperation("$diff", id -> id.isVersionIdPartValidLong() && id.getVersionIdPartAsLong() > 1)
+
 			.addServer()
 				.withId("hapi")
-				.withFhirVersion(FhirVersionEnum.DSTU2)
-				.withBaseUrl("http://fhirtest.uhn.ca/baseDstu2")
-				.withName("Public HAPI Test Server");
-		
+				.withFhirVersion(FhirVersionEnum.R4)
+				.withBaseUrl("http://hapi.fhir.org/baseR4")
+				.withName("Public HAPI Test Server")
+				// Disable the read and update buttons on search result rows for this server
+				.withSearchResultRowInteraction(RestOperationTypeEnum.READ, id -> false)
+				.withSearchResultRowInteraction(RestOperationTypeEnum.UPDATE, id -> false);
+
 		/*
 		 * Use the method below to supply a client "factory" which can be used 
 		 * if your server requires authentication
diff --git a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/RestfulPatientResourceProviderMore.java b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/RestfulPatientResourceProviderMore.java
index 9113e570312..10d7735293a 100644
--- a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/RestfulPatientResourceProviderMore.java
+++ b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/RestfulPatientResourceProviderMore.java
@@ -23,8 +23,10 @@ package ca.uhn.hapi.fhir.docs;
 import ca.uhn.fhir.i18n.Msg;
 import ca.uhn.fhir.context.FhirContext;
 import ca.uhn.fhir.model.api.Include;
+import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
 import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
 import ca.uhn.fhir.model.api.annotation.Description;
+import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum;
 import ca.uhn.fhir.parser.DataFormatException;
 import ca.uhn.fhir.rest.annotation.Count;
 import ca.uhn.fhir.rest.annotation.*;
@@ -392,17 +394,33 @@ public List getPatientHistory(
    List retVal = new ArrayList();
    
    Patient patient = new Patient();
-   patient.addName().setFamily("Smith");
-   
+
    // Set the ID and version
    patient.setId(theId.withVersion("1"));
-   
-   // ...populate the rest...
+
+	if (isDeleted(patient)) {
+
+		// If the resource is deleted, it just needs to have an ID and some metadata
+		ResourceMetadataKeyEnum.DELETED_AT.put(patient, InstantType.withCurrentTime());
+		ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(patient, BundleEntryTransactionMethodEnum.DELETE);
+
+	} else {
+
+		// If the resource is not deleted, it should have normal resource content
+		patient.addName().setFamily("Smith"); // ..populate the rest
+
+	}
+
    return retVal;
 }
 //END SNIPPET: history
 
 
+	private boolean isDeleted(Patient thePatient) {
+		return false;
+	}
+
+
 //START SNIPPET: vread
 @Read(version=true)
 public Patient readOrVread(@IdParam IdType theId) {
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4456-add-customizable-overlay-buttons.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4456-add-customizable-overlay-buttons.yaml
new file mode 100644
index 00000000000..aadaca65fe7
--- /dev/null
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4456-add-customizable-overlay-buttons.yaml
@@ -0,0 +1,8 @@
+---
+type: add
+issue: 4456
+title: "The Testpage Overlay module now supports customizable operation buttons
+   on search results. For example, you can configure each row of the search
+   results page to include a `$validate` button and a `$diff` button, or
+   choose other operations relevant to your use case. Currently only
+   operations which do not require any parameters are possible."
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4456-history-deleted-on-hashmapresourceprovider.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4456-history-deleted-on-hashmapresourceprovider.yaml
new file mode 100644
index 00000000000..ffb2b464f69
--- /dev/null
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4456-history-deleted-on-hashmapresourceprovider.yaml
@@ -0,0 +1,5 @@
+---
+type: fix
+issue: 4456
+title: "The HashMapResourceProvider failed to display history results if the history
+  included deleted resources. This has been corrected."
diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4456-operationoutcome-narrative-tweak.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4456-operationoutcome-narrative-tweak.yaml
new file mode 100644
index 00000000000..4974492acf0
--- /dev/null
+++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_4_0/4456-operationoutcome-narrative-tweak.yaml
@@ -0,0 +1,6 @@
+---
+type: change
+issue: 4456
+title: "The built-in narrative template for the OperationOutcome resource no longer renders
+   the _OperationOutcome.issue.diagnostic_ inside a <pre> tag, which made long
+   messages hard to read."
diff --git a/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java b/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java
index 23f15cf4cb7..2dbffa071bf 100644
--- a/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java
+++ b/hapi-fhir-jpaserver-test-dstu3/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java
@@ -4749,7 +4749,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
 			ourLog.info(resp);
 			assertEquals(200, response.getStatusLine().getStatusCode());
 			assertThat(resp, not(containsString("Resource has no id")));
-			assertThat(resp, containsString("
No issues detected during validation
")); + assertThat(resp, containsString("No issues detected during validation")); assertThat(resp, stringContainsInOrder("", "", "", "", "")); @@ -4776,7 +4776,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test { ourLog.info(resp); assertEquals(200, response.getStatusLine().getStatusCode()); assertThat(resp, not(containsString("Resource has no id"))); - assertThat(resp, containsString("
No issues detected during validation
")); + assertThat(resp, containsString("No issues detected during validation")); assertThat(resp, stringContainsInOrder("", "", "", "", "")); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java index 086dc8f91ac..d99ff943d0e 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ResourceProviderR4Test.java @@ -7011,7 +7011,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test { ourLog.info(resp); assertEquals(200, response.getStatusLine().getStatusCode()); assertThat(resp, not(containsString("Resource has no id"))); - assertThat(resp, containsString("
No issues detected during validation
")); + assertThat(resp, containsString("No issues detected during validation")); assertThat(resp, stringContainsInOrder("", "", "", "", "")); diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/ScheduledSubscriptionDeleter.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/ScheduledSubscriptionDeleter.java new file mode 100644 index 00000000000..4ad99263670 --- /dev/null +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/ScheduledSubscriptionDeleter.java @@ -0,0 +1,77 @@ +package ca.uhn.fhirtest; + +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import org.apache.commons.lang3.time.DateUtils; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.EventListener; + +import java.util.Date; +import java.util.Timer; +import java.util.TimerTask; + +/** + * This is just a quick and dirty utility class to purge subscriptions on the + * public test server after 1 day. It uses a Timer to automatically check for old + * subscriptions periodically, and if it finds any with a lastUpdated date more than + * 24 hours ago it deletes them. This is to prevent people's subscription testing + * from hanging around and gumming up the server. + */ +public class ScheduledSubscriptionDeleter { + + private static final Logger ourLog = LoggerFactory.getLogger(ScheduledSubscriptionDeleter.class); + + @Autowired + private DaoRegistry myDaoRegistry; + + private Timer myTimer; + + @EventListener(ContextRefreshedEvent.class) + public void start() { + if (myTimer == null) { + myTimer = new Timer(); + myTimer.scheduleAtFixedRate(new MyTimerTask(), 0, DateUtils.MILLIS_PER_HOUR); + } + } + + @EventListener(ContextClosedEvent.class) + public void stop() { + myTimer.cancel(); + myTimer = null; + } + + private class MyTimerTask extends TimerTask { + + @Override + public void run() { + deleteOldSubscriptions(); + } + + private void deleteOldSubscriptions() { + if (myDaoRegistry.isResourceTypeSupported("Subscription")) { + int count = 10; + Date cutoff = DateUtils.addDays(new Date(), -1); + + IFhirResourceDao subscriptionDao = myDaoRegistry.getResourceDao("Subscription"); + SearchParameterMap params = SearchParameterMap + .newSynchronous() + .setCount(count); + IBundleProvider subscriptions = subscriptionDao.search(params, new SystemRequestDetails()); + for (IBaseResource next : subscriptions.getResources(0, count)) { + if (next.getMeta().getLastUpdated().before(cutoff)) { + ourLog.info("Auto deleting old subscription: {}", next.getIdElement()); + subscriptionDao.delete(next.getIdElement().toUnqualifiedVersionless(), new SystemRequestDetails()); + } + } + } + } + } +} diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/CommonConfig.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/CommonConfig.java index 65f4bdc91dc..760bc27bfec 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/CommonConfig.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/CommonConfig.java @@ -12,6 +12,7 @@ import ca.uhn.fhir.jpa.subscription.match.deliver.email.IEmailSender; import ca.uhn.fhir.jpa.subscription.submit.config.SubscriptionSubmitterConfig; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor; +import ca.uhn.fhirtest.ScheduledSubscriptionDeleter; import ca.uhn.fhirtest.interceptor.AnalyticsInterceptor; import ca.uhn.fhirtest.joke.HolyFooCowInterceptor; import org.springframework.context.annotation.Bean; @@ -99,4 +100,9 @@ public class CommonConfig { return "true".equalsIgnoreCase(System.getProperty("testmode.local")); } + @Bean + public ScheduledSubscriptionDeleter scheduledSubscriptionDeleter() { + return new ScheduledSubscriptionDeleter(); + } + } diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/FhirTesterConfig.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/FhirTesterConfig.java index 9e7f03f4d7f..a7a1deb89df 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/FhirTesterConfig.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/FhirTesterConfig.java @@ -1,25 +1,24 @@ package ca.uhn.fhirtest.config; -import org.springframework.beans.factory.annotation.Autowire; -import org.springframework.context.annotation.*; - import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.to.FhirTesterMvcConfig; import ca.uhn.fhir.to.TesterConfig; import ca.uhn.fhirtest.mvc.SubscriptionPlaygroundController; +import org.springframework.beans.factory.annotation.Autowire; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import static ca.uhn.fhir.rest.api.Constants.EXTOP_VALIDATE; -//@formatter:off /** * This spring config file configures the web testing module. It serves two * purposes: * 1. It imports FhirTesterMvcConfig, which is the spring config for the - * tester itself + * tester itself * 2. It tells the tester which server(s) to talk to, via the testerConfig() - * method below + * method below */ -//@Configuration -//@Import(FhirTesterMvcConfig.class) -//@ComponentScan(basePackages = "ca.uhn.fhirtest.mvc") @Configuration @Import(FhirTesterMvcConfig.class) public class FhirTesterConfig { @@ -27,15 +26,15 @@ public class FhirTesterConfig { /** * This bean tells the testing webpage which servers it should configure itself * to communicate with. In this example we configure it to talk to the local - * server, as well as one public server. If you are creating a project to - * deploy somewhere else, you might choose to only put your own server's + * server, as well as one public server. If you are creating a project to + * deploy somewhere else, you might choose to only put your own server's * address here. - * + *

* Note the use of the ${serverBase} variable below. This will be replaced with * the base URL as reported by the server itself. Often for a simple Tomcat * (or other container) installation, this will end up being something * like "http://localhost:8080/hapi-fhir-jpaserver-example". If you are - * deploying your server to a place with a fully qualified domain name, + * deploying your server to a place with a fully qualified domain name, * you might want to use that instead of using the variable. */ @Bean @@ -43,70 +42,81 @@ public class FhirTesterConfig { TesterConfig retVal = new TesterConfig(); retVal .addServer() - .withId("home_r4") - .withFhirVersion(FhirVersionEnum.R4) - .withBaseUrl("http://hapi.fhir.org/baseR4") - .withName("HAPI Test Server (R4 FHIR)") + .withId("home_r4") + .withFhirVersion(FhirVersionEnum.R4) + .withBaseUrl("http://hapi.fhir.org/baseR4") + .withName("HAPI Test Server (R4 FHIR)") + .withSearchResultRowOperation(EXTOP_VALIDATE, id -> true) + .withSearchResultRowOperation("$diff", id -> id.isVersionIdPartValidLong() && id.getVersionIdPartAsLong() > 1) + .withSearchResultRowOperation("$everything", id -> "Patient".equals(id.getResourceType())) + .addServer() - .withId("home_r4b") - .withFhirVersion(FhirVersionEnum.R4B) - .withBaseUrl("http://hapi.fhir.org/baseR4B") - .withName("HAPI Test Server (R4B FHIR)") + .withId("home_r4b") + .withFhirVersion(FhirVersionEnum.R4B) + .withBaseUrl("http://hapi.fhir.org/baseR4B") + .withName("HAPI Test Server (R4B FHIR)") + .withSearchResultRowOperation(EXTOP_VALIDATE, id -> true) + .withSearchResultRowOperation("$diff", id -> id.isVersionIdPartValidLong() && id.getVersionIdPartAsLong() > 1) + .withSearchResultRowOperation("$everything", id -> "Patient".equals(id.getResourceType())) + .addServer() - .withId("home_21") - .withFhirVersion(FhirVersionEnum.DSTU3) - .withBaseUrl("http://hapi.fhir.org/baseDstu3") - .withName("HAPI Test Server (STU3 FHIR)") + .withId("home_21") + .withFhirVersion(FhirVersionEnum.DSTU3) + .withBaseUrl("http://hapi.fhir.org/baseDstu3") + .withName("HAPI Test Server (STU3 FHIR)") + .withSearchResultRowOperation(EXTOP_VALIDATE, id -> true) + .withSearchResultRowOperation("$diff", id -> id.isVersionIdPartValidLong() && id.getVersionIdPartAsLong() > 1) + .withSearchResultRowOperation("$everything", id -> "Patient".equals(id.getResourceType())) + .addServer() - .withId("hapi_dev") - .withFhirVersion(FhirVersionEnum.DSTU2) - .withBaseUrl("http://hapi.fhir.org/baseDstu2") - .withName("HAPI Test Server (DSTU2 FHIR)") + .withId("hapi_dev") + .withFhirVersion(FhirVersionEnum.DSTU2) + .withBaseUrl("http://hapi.fhir.org/baseDstu2") + .withName("HAPI Test Server (DSTU2 FHIR)") + .withSearchResultRowOperation(EXTOP_VALIDATE, id -> true) + .withSearchResultRowOperation("$everything", id -> "Patient".equals(id.getResourceType())) + .addServer() - .withId("home_r5") - .withFhirVersion(FhirVersionEnum.R5) - .withBaseUrl("http://hapi.fhir.org/baseR5") - .withName("HAPI Test Server (R5 FHIR)") -// .addServer() -// .withId("tdl_d2") -// .withFhirVersion(FhirVersionEnum.DSTU2) -// .withBaseUrl("http://hapi.fhir.org/testDataLibraryDstu2") -// .withName("Test Data Library (DSTU2 FHIR)") -// .allowsApiKey() -// .addServer() -// .withId("tdl_d3") -// .withFhirVersion(FhirVersionEnum.DSTU3) -// .withBaseUrl("http://hapi.fhir.org/testDataLibraryStu3") -// .withName("Test Data Library (DSTU3 FHIR)") -// .allowsApiKey() + .withId("home_r5") + .withFhirVersion(FhirVersionEnum.R5) + .withBaseUrl("http://hapi.fhir.org/baseR5") + .withName("HAPI Test Server (R5 FHIR)") + .withSearchResultRowOperation(EXTOP_VALIDATE, id -> true) + .withSearchResultRowOperation("$diff", id -> id.isVersionIdPartValidLong() && id.getVersionIdPartAsLong() > 1) + .withSearchResultRowOperation("$everything", id -> "Patient".equals(id.getResourceType())) + + // Non-HAPI servers follow + .addServer() - .withId("hi4") - .withFhirVersion(FhirVersionEnum.DSTU3) - .withBaseUrl("http://test.fhir.org/r4") - .withName("Health Intersections (R4 FHIR)") + .withId("hi4") + .withFhirVersion(FhirVersionEnum.DSTU3) + .withBaseUrl("http://test.fhir.org/r4") + .withName("Health Intersections (R4 FHIR)") + .addServer() - .withId("hi3") - .withFhirVersion(FhirVersionEnum.DSTU3) - .withBaseUrl("http://test.fhir.org/r3") - .withName("Health Intersections (STU3 FHIR)") + .withId("hi3") + .withFhirVersion(FhirVersionEnum.DSTU3) + .withBaseUrl("http://test.fhir.org/r3") + .withName("Health Intersections (STU3 FHIR)") + .addServer() - .withId("hi2") - .withFhirVersion(FhirVersionEnum.DSTU2) - .withBaseUrl("http://test.fhir.org/r2") - .withName("Health Intersections (DSTU2 FHIR)") + .withId("hi2") + .withFhirVersion(FhirVersionEnum.DSTU2) + .withBaseUrl("http://test.fhir.org/r2") + .withName("Health Intersections (DSTU2 FHIR)") + .addServer() - .withId("spark2") - .withFhirVersion(FhirVersionEnum.DSTU3) - .withBaseUrl("http://vonk.fire.ly/") - .withName("Vonk - Firely (STU3 FHIR)"); - + .withId("spark2") + .withFhirVersion(FhirVersionEnum.DSTU3) + .withBaseUrl("http://vonk.fire.ly/") + .withName("Vonk - Firely (STU3 FHIR)"); + return retVal; } - - @Bean(autowire=Autowire.BY_TYPE) + + @Bean(autowire = Autowire.BY_TYPE) public SubscriptionPlaygroundController subscriptionPlaygroundController() { return new SubscriptionPlaygroundController(); } - + } -//@formatter:on diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/HistoryMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/HistoryMethodBinding.java index 540befbb6c9..793ba33804d 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/HistoryMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/HistoryMethodBinding.java @@ -191,7 +191,7 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding { } if (isBlank(nextResource.getIdElement().getVersionIdPart()) && nextResource instanceof IResource) { //TODO: Use of a deprecated method should be resolved. - IdDt versionId = ResourceMetadataKeyEnum.VERSION_ID.get((IResource) nextResource); + IdDt versionId = ResourceMetadataKeyEnum.VERSION_ID.get(nextResource); if (versionId == null || versionId.isEmpty()) { throw new InternalErrorException(Msg.code(411) + "Server provided resource at index " + index + " with no Version ID set (using IResource#setId(IdDt))"); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/HashMapResourceProvider.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/HashMapResourceProvider.java index e57d19fa45d..ce3b17905a4 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/HashMapResourceProvider.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/HashMapResourceProvider.java @@ -20,17 +20,16 @@ package ca.uhn.fhir.rest.server.provider; * #L% */ -import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.context.BaseRuntimeChildDefinition; -import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.api.HookParams; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.interceptor.model.RequestPartitionId; -import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; +import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum; import ca.uhn.fhir.rest.annotation.ConditionalUrlParam; import ca.uhn.fhir.rest.annotation.Create; import ca.uhn.fhir.rest.annotation.Delete; @@ -68,6 +67,7 @@ import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.Collections; +import java.util.Date; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; @@ -103,10 +103,10 @@ public class HashMapResourceProvider implements IResour protected LinkedList myTypeHistory = new LinkedList<>(); protected AtomicLong mySearchCount = new AtomicLong(0); private long myNextId; - private AtomicLong myDeleteCount = new AtomicLong(0); - private AtomicLong myUpdateCount = new AtomicLong(0); - private AtomicLong myCreateCount = new AtomicLong(0); - private AtomicLong myReadCount = new AtomicLong(0); + private final AtomicLong myDeleteCount = new AtomicLong(0); + private final AtomicLong myUpdateCount = new AtomicLong(0); + private final AtomicLong myCreateCount = new AtomicLong(0); + private final AtomicLong myReadCount = new AtomicLong(0); /** * Constructor @@ -134,7 +134,7 @@ public class HashMapResourceProvider implements IResour /** * Clear the counts used by {@link #getCountRead()} and other count methods */ - public synchronized void clearCounts() { + public synchronized void clearCounts() { myReadCount.set(0L); myUpdateCount.set(0L); myCreateCount.set(0L); @@ -163,12 +163,12 @@ public class HashMapResourceProvider implements IResour assert !myIdToVersionToResourceMap.containsKey(idPartAsString); - IIdType id = store(theResource, idPartAsString, versionIdPart, theRequestDetails, theTransactionDetails); - theResource.setId(id); + store(theResource, idPartAsString, versionIdPart, theRequestDetails, theTransactionDetails, false); } + @SuppressWarnings({"unchecked"}) @Delete - public synchronized MethodOutcome delete(@IdParam IIdType theId, RequestDetails theRequestDetails) { + public synchronized MethodOutcome delete(@IdParam IIdType theId, RequestDetails theRequestDetails) { TransactionDetails transactionDetails = new TransactionDetails(); TreeMap versions = myIdToVersionToResourceMap.get(theId.getIdPart()); @@ -176,9 +176,9 @@ public class HashMapResourceProvider implements IResour throw new ResourceNotFoundException(Msg.code(1979) + theId); } - + T deletedInstance = (T) myFhirContext.getResourceDefinition(myResourceType).newInstance(); long nextVersion = versions.lastEntry().getKey() + 1L; - IIdType id = store(null, theId.getIdPart(), nextVersion, theRequestDetails, transactionDetails); + IIdType id = store(deletedInstance, theId.getIdPart(), nextVersion, theRequestDetails, transactionDetails, true); myDeleteCount.incrementAndGet(); @@ -190,7 +190,7 @@ public class HashMapResourceProvider implements IResour * This method returns a simple operation count. This is mostly * useful for testing purposes. */ - public synchronized long getCountCreate() { + public synchronized long getCountCreate() { return myCreateCount.get(); } @@ -198,7 +198,7 @@ public class HashMapResourceProvider implements IResour * This method returns a simple operation count. This is mostly * useful for testing purposes. */ - public synchronized long getCountDelete() { + public synchronized long getCountDelete() { return myDeleteCount.get(); } @@ -206,7 +206,7 @@ public class HashMapResourceProvider implements IResour * This method returns a simple operation count. This is mostly * useful for testing purposes. */ - public synchronized long getCountRead() { + public synchronized long getCountRead() { return myReadCount.get(); } @@ -214,7 +214,7 @@ public class HashMapResourceProvider implements IResour * This method returns a simple operation count. This is mostly * useful for testing purposes. */ - public synchronized long getCountSearch() { + public synchronized long getCountSearch() { return mySearchCount.get(); } @@ -222,7 +222,7 @@ public class HashMapResourceProvider implements IResour * This method returns a simple operation count. This is mostly * useful for testing purposes. */ - public synchronized long getCountUpdate() { + public synchronized long getCountUpdate() { return myUpdateCount.get(); } @@ -237,7 +237,7 @@ public class HashMapResourceProvider implements IResour } @History - public synchronized List historyInstance(@IdParam IIdType theId, RequestDetails theRequestDetails) { + public synchronized List historyInstance(@IdParam IIdType theId, RequestDetails theRequestDetails) { LinkedList retVal = myIdToHistory.get(theId.getIdPart()); if (retVal == null) { throw new ResourceNotFoundException(Msg.code(1980) + theId); @@ -252,7 +252,7 @@ public class HashMapResourceProvider implements IResour } @Read(version = true) - public synchronized T read(@IdParam IIdType theId, RequestDetails theRequestDetails) { + public synchronized T read(@IdParam IIdType theId, RequestDetails theRequestDetails) { TreeMap versions = myIdToVersionToResourceMap.get(theId.getIdPart()); if (versions == null || versions.isEmpty()) { throw new ResourceNotFoundException(Msg.code(1981) + theId); @@ -265,7 +265,7 @@ public class HashMapResourceProvider implements IResour throw new ResourceNotFoundException(Msg.code(1982) + theId); } else { T resource = versions.get(versionId); - if (resource == null) { + if (resource == null || ResourceMetadataKeyEnum.DELETED_AT.get(resource) != null) { throw new ResourceGoneException(Msg.code(1983) + theId); } retVal = resource; @@ -285,21 +285,26 @@ public class HashMapResourceProvider implements IResour } @Search - public synchronized List searchAll(RequestDetails theRequestDetails) { + public synchronized List searchAll(RequestDetails theRequestDetails) { mySearchCount.incrementAndGet(); List retVal = getAllResources(); return fireInterceptorsAndFilterAsNeeded(retVal, theRequestDetails); } @Nonnull - protected synchronized List getAllResources() { + protected synchronized List getAllResources() { List retVal = new ArrayList<>(); for (TreeMap next : myIdToVersionToResourceMap.values()) { if (next.isEmpty() == false) { T nextResource = next.lastEntry().getValue(); if (nextResource != null) { - retVal.add(nextResource); + if (ResourceMetadataKeyEnum.DELETED_AT.get(nextResource) == null) { + // Clone the resource for search results so that the + // stored metadata doesn't appear in the results + T nextResourceClone = myFhirContext.newTerser().clone(nextResource); + retVal.add(nextResourceClone); + } } } } @@ -308,7 +313,7 @@ public class HashMapResourceProvider implements IResour } @Search - public synchronized List searchById( + public synchronized List searchById( @RequiredParam(name = "_id") TokenAndListParam theIds, RequestDetails theRequestDetails) { List retVal = new ArrayList<>(); @@ -345,12 +350,25 @@ public class HashMapResourceProvider implements IResour return fireInterceptorsAndFilterAsNeeded(retVal, theRequestDetails); } - private IIdType store(@ResourceParam T theResource, String theIdPart, Long theVersionIdPart, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails) { + @SuppressWarnings({"unchecked", "DataFlowIssue"}) + private IIdType store(@Nonnull T theResource, String theIdPart, Long theVersionIdPart, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails, boolean theDeleted) { IIdType id = myFhirContext.getVersion().newIdType(); String versionIdPart = Long.toString(theVersionIdPart); id.setParts(null, myResourceName, theIdPart, versionIdPart); - if (theResource != null) { - theResource.setId(id); + theResource.setId(id); + + if (theDeleted) { + IPrimitiveType deletedAt = (IPrimitiveType) myFhirContext.getElementDefinition("instant").newInstance(); + deletedAt.setValue(new Date()); + ResourceMetadataKeyEnum.DELETED_AT.put(theResource, deletedAt); + ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(theResource, BundleEntryTransactionMethodEnum.DELETE); + } else { + ResourceMetadataKeyEnum.DELETED_AT.put(theResource, null); + if (theVersionIdPart > 1) { + ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(theResource, BundleEntryTransactionMethodEnum.PUT); + } else { + ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(theResource, BundleEntryTransactionMethodEnum.POST); + } } /* @@ -358,22 +376,13 @@ public class HashMapResourceProvider implements IResour * in the resource being stored accurately represents the version * that was assigned by this provider */ - if (theResource != null) { - if (myFhirContext.getVersion().getVersion() == FhirVersionEnum.DSTU2) { - ResourceMetadataKeyEnum.VERSION.put((IResource) theResource, versionIdPart); - } else { - BaseRuntimeChildDefinition metaChild = myFhirContext.getResourceDefinition(myResourceType).getChildByName("meta"); - List metaValues = metaChild.getAccessor().getValues(theResource); - if (metaValues.size() > 0) { - IBase meta = metaValues.get(0); - BaseRuntimeElementCompositeDefinition metaDef = (BaseRuntimeElementCompositeDefinition) myFhirContext.getElementDefinition(meta.getClass()); - BaseRuntimeChildDefinition versionIdDef = metaDef.getChildByName("versionId"); - List versionIdValues = versionIdDef.getAccessor().getValues(meta); - if (versionIdValues.size() > 0) { - IPrimitiveType versionId = (IPrimitiveType) versionIdValues.get(0); - versionId.setValueAsString(versionIdPart); - } - } + if (myFhirContext.getVersion().getVersion() == FhirVersionEnum.DSTU2) { + ResourceMetadataKeyEnum.VERSION.put(theResource, versionIdPart); + } else { + BaseRuntimeChildDefinition metaChild = myFhirContext.getResourceDefinition(myResourceType).getChildByName("meta"); + List metaValues = metaChild.getAccessor().getValues(theResource); + if (metaValues.size() > 0) { + theResource.getMeta().setVersionId(versionIdPart); } } @@ -383,52 +392,70 @@ public class HashMapResourceProvider implements IResour TreeMap versionToResource = getVersionToResource(theIdPart); versionToResource.put(theVersionIdPart, theResource); - if (theRequestDetails != null) { + if (theRequestDetails != null && theRequestDetails.getInterceptorBroadcaster() != null) { IInterceptorBroadcaster interceptorBroadcaster = theRequestDetails.getInterceptorBroadcaster(); - if (theResource != null) { - if (!myIdToHistory.containsKey(theIdPart)) { + if (theDeleted) { - // Interceptor call: STORAGE_PRESTORAGE_RESOURCE_CREATED - HookParams preStorageParams = new HookParams() - .add(RequestDetails.class, theRequestDetails) - .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) - .add(IBaseResource.class, theResource) - .add(RequestPartitionId.class, null) // we should add this if we want - but this is test usage - .add(TransactionDetails.class, theTransactionDetails); - interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, preStorageParams); + // Interceptor call: STORAGE_PRESTORAGE_RESOURCE_DELETED + HookParams preStorageParams = new HookParams() + .add(RequestDetails.class, theRequestDetails) + .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) + .add(IBaseResource.class, myIdToHistory.get(theIdPart).getFirst()) + .add(TransactionDetails.class, theTransactionDetails); + interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, preStorageParams); - // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_CREATED - HookParams preCommitParams = new HookParams() - .add(RequestDetails.class, theRequestDetails) - .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) - .add(IBaseResource.class, theResource) - .add(TransactionDetails.class, theTransactionDetails) - .add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)); - interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED, preCommitParams); + // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_DELETED + HookParams preCommitParams = new HookParams() + .add(RequestDetails.class, theRequestDetails) + .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) + .add(IBaseResource.class, myIdToHistory.get(theIdPart).getFirst()) + .add(TransactionDetails.class, theTransactionDetails) + .add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)); + interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, preCommitParams); - } else { - // Interceptor call: STORAGE_PRESTORAGE_RESOURCE_UPDATED - HookParams preStorageParams = new HookParams() - .add(RequestDetails.class, theRequestDetails) - .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) - .add(IBaseResource.class, myIdToHistory.get(theIdPart).getFirst()) - .add(IBaseResource.class, theResource) - .add(TransactionDetails.class, theTransactionDetails); - interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, preStorageParams); + } else if (!myIdToHistory.containsKey(theIdPart)) { - // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_UPDATED - HookParams preCommitParams = new HookParams() - .add(RequestDetails.class, theRequestDetails) - .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) - .add(IBaseResource.class, myIdToHistory.get(theIdPart).getFirst()) - .add(IBaseResource.class, theResource) - .add(TransactionDetails.class, theTransactionDetails) - .add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)); - interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, preCommitParams); + // Interceptor call: STORAGE_PRESTORAGE_RESOURCE_CREATED + HookParams preStorageParams = new HookParams() + .add(RequestDetails.class, theRequestDetails) + .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) + .add(IBaseResource.class, theResource) + .add(RequestPartitionId.class, null) // we should add this if we want - but this is test usage + .add(TransactionDetails.class, theTransactionDetails); + interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, preStorageParams); + + // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_CREATED + HookParams preCommitParams = new HookParams() + .add(RequestDetails.class, theRequestDetails) + .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) + .add(IBaseResource.class, theResource) + .add(TransactionDetails.class, theTransactionDetails) + .add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)); + interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED, preCommitParams); + + } else { + + // Interceptor call: STORAGE_PRESTORAGE_RESOURCE_UPDATED + HookParams preStorageParams = new HookParams() + .add(RequestDetails.class, theRequestDetails) + .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) + .add(IBaseResource.class, myIdToHistory.get(theIdPart).getFirst()) + .add(IBaseResource.class, theResource) + .add(TransactionDetails.class, theTransactionDetails); + interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, preStorageParams); + + // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_UPDATED + HookParams preCommitParams = new HookParams() + .add(RequestDetails.class, theRequestDetails) + .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) + .add(IBaseResource.class, myIdToHistory.get(theIdPart).getFirst()) + .add(IBaseResource.class, theResource) + .add(TransactionDetails.class, theTransactionDetails) + .add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)); + interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, preCommitParams); - } } } @@ -447,7 +474,7 @@ public class HashMapResourceProvider implements IResour * @param theConditional This is provided only so that subclasses can implement if they want */ @Update - public synchronized MethodOutcome update( + public synchronized MethodOutcome update( @ResourceParam T theResource, @ConditionalUrlParam String theConditional, RequestDetails theRequestDetails) { @@ -478,7 +505,7 @@ public class HashMapResourceProvider implements IResour created = false; } - IIdType id = store(theResource, idPartAsString, versionIdPart, theRequestDetails, theTransactionDetails); + IIdType id = store(theResource, idPartAsString, versionIdPart, theRequestDetails, theTransactionDetails, false); theResource.setId(id); return created; } @@ -495,7 +522,7 @@ public class HashMapResourceProvider implements IResour * @param theResource The resource to store. If the resource has an ID, that ID is updated. * @return Return the ID assigned to the stored resource */ - public synchronized IIdType store(T theResource) { + public synchronized IIdType store(T theResource) { if (theResource.getIdElement().hasIdPart()) { updateInternal(theResource, null, new TransactionDetails()); } else { @@ -509,7 +536,7 @@ public class HashMapResourceProvider implements IResour * * @since 4.1.0 */ - public synchronized List getStoredResources() { + public synchronized List getStoredResources() { List retVal = new ArrayList<>(); for (TreeMap next : myIdToVersionToResourceMap.values()) { retVal.add(next.lastEntry().getValue()); diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorDstu2Test.java index d06534bd888..e809ef4a6bc 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorDstu2Test.java @@ -103,7 +103,7 @@ public class DefaultThymeleafNarrativeGeneratorDstu2Test { ourLog.info(output); - assertThat(output, containsString("

YThis is a warning
")); + assertThat(output, containsString("YThis is a warning")); } @Test diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorDstu3Test.java index b60fb81274b..421a0ce66a0 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorDstu3Test.java @@ -163,7 +163,7 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test { ourLog.info(output); - assertThat(output, containsString("
YThis is a warning
")); + assertThat(output, containsString("YThis is a warning")); } @Test diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorR4Test.java index 178e09ff654..6a88ca6c9e8 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorR4Test.java @@ -111,7 +111,7 @@ public class DefaultThymeleafNarrativeGeneratorR4Test { ourLog.info(output); - assertThat(output, containsString("
YThis is a warning
")); + assertThat(output, containsString("YThis is a warning")); } @Test diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/provider/HashMapResourceProviderTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/provider/HashMapResourceProviderTest.java index 975bbedc858..526ef9dab6b 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/provider/HashMapResourceProviderTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/provider/HashMapResourceProviderTest.java @@ -3,35 +3,31 @@ package ca.uhn.fhir.rest.server.provider; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor; import ca.uhn.fhir.interceptor.api.Pointcut; -import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; -import ca.uhn.fhir.rest.gclient.IDeleteTyped; -import ca.uhn.fhir.rest.server.IResourceProvider; -import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import ca.uhn.fhir.test.utilities.JettyUtil; +import ca.uhn.fhir.rest.server.interceptor.ResponseValidatingInterceptor; +import ca.uhn.fhir.test.utilities.server.HashMapResourceProviderExtension; +import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; import ca.uhn.fhir.util.TestUtil; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; +import ca.uhn.fhir.validation.FhirValidator; +import ca.uhn.fhir.validation.ResultSeverityEnum; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Patient; import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.servlet.ServletException; import java.util.List; import java.util.stream.Collectors; @@ -40,6 +36,8 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.matchesPattern; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -48,24 +46,22 @@ import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) public class HashMapResourceProviderTest { + private static final FhirContext ourCtx = FhirContext.forR4Cached(); + @RegisterExtension + @Order(0) + private static final RestfulServerExtension ourRestServer = new RestfulServerExtension(ourCtx); + @RegisterExtension + @Order(1) + private static final HashMapResourceProviderExtension myPatientResourceProvider = new HashMapResourceProviderExtension<>(ourRestServer, Patient.class); + @RegisterExtension + @Order(2) + private static final HashMapResourceProviderExtension myObservationResourceProvider = new HashMapResourceProviderExtension<>(ourRestServer, Observation.class); + private static final Logger ourLog = LoggerFactory.getLogger(HashMapResourceProviderTest.class); - private static MyRestfulServer ourRestServer; - private static Server ourListenerServer; - private static IGenericClient ourClient; - private static FhirContext ourCtx = FhirContext.forR4(); - private static HashMapResourceProvider myPatientResourceProvider; - private static HashMapResourceProvider myObservationResourceProvider; @Mock private IAnonymousInterceptor myAnonymousInterceptor; - @BeforeEach - public void before() { - ourRestServer.clearData(); - myPatientResourceProvider.clearCounts(); - myObservationResourceProvider.clearCounts(); - } - @Test public void testCreateAndRead() { ourRestServer.getInterceptorService().registerAnonymousInterceptor(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, myAnonymousInterceptor); @@ -74,7 +70,7 @@ public class HashMapResourceProviderTest { // Create Patient p = new Patient(); p.setActive(true); - IIdType id = ourClient.create().resource(p).execute().getId(); + IIdType id = ourRestServer.getFhirClient().create().resource(p).execute().getId(); assertThat(id.getIdPart(), matchesPattern("[0-9]+")); assertEquals("1", id.getVersionIdPart()); @@ -82,8 +78,8 @@ public class HashMapResourceProviderTest { verify(myAnonymousInterceptor, Mockito.times(1)).invoke(eq(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED), any()); // Read - p = (Patient) ourClient.read().resource("Patient").withId(id).execute(); - assertEquals(true, p.getActive()); + p = (Patient) ourRestServer.getFhirClient().read().resource("Patient").withId(id).execute(); + assertTrue(p.getActive()); assertEquals(1, myPatientResourceProvider.getCountRead()); } @@ -94,13 +90,13 @@ public class HashMapResourceProviderTest { Patient p = new Patient(); p.setId("ABC"); p.setActive(true); - IIdType id = ourClient.update().resource(p).execute().getId(); + IIdType id = ourRestServer.getFhirClient().update().resource(p).execute().getId(); assertEquals("ABC", id.getIdPart()); assertEquals("1", id.getVersionIdPart()); // Read - p = (Patient) ourClient.read().resource("Patient").withId(id).execute(); - assertEquals(true, p.getActive()); + p = (Patient) ourRestServer.getFhirClient().read().resource("Patient").withId(id).execute(); + assertTrue(p.getActive()); } @Test @@ -108,31 +104,39 @@ public class HashMapResourceProviderTest { // Create Patient p = new Patient(); p.setActive(true); - IIdType id = ourClient.create().resource(p).execute().getId(); + IIdType id = ourRestServer.getFhirClient().create().resource(p).execute().getId().toUnqualified(); assertThat(id.getIdPart(), matchesPattern("[0-9]+")); assertEquals("1", id.getVersionIdPart()); assertEquals(0, myPatientResourceProvider.getCountDelete()); - IDeleteTyped iDeleteTyped = ourClient.delete().resourceById(id.toUnqualifiedVersionless()); + ourRestServer.getFhirClient().delete().resourceById(id.toUnqualifiedVersionless()).execute(); ourLog.info("About to execute"); - try { - iDeleteTyped.execute(); - } catch (NullPointerException e) { - ourLog.error("NPE", e); - fail(e.toString()); - } assertEquals(1, myPatientResourceProvider.getCountDelete()); // Read - ourClient.read().resource("Patient").withId(id.withVersion("1")).execute(); + ourRestServer.getFhirClient().read().resource("Patient").withId(id.withVersion("1")).execute(); try { - ourClient.read().resource("Patient").withId(id.withVersion("2")).execute(); + ourRestServer.getFhirClient().read().resource("Patient").withId(id.withVersion("2")).execute(); fail(); } catch (ResourceGoneException e) { // good } + + // History should include deleted entry + Bundle history = ourRestServer.getFhirClient().history().onType(Patient.class).returnBundle(Bundle.class).execute(); + ourLog.info("History:\n{}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(history)); + assertEquals(id.withVersion("2").getValue(), history.getEntry().get(0).getRequest().getUrl()); + assertEquals("DELETE", history.getEntry().get(0).getRequest().getMethod().toCode()); + assertEquals(id.withVersion("1").getValue(), history.getEntry().get(1).getRequest().getUrl()); + assertEquals("POST", history.getEntry().get(1).getRequest().getMethod().toCode()); + + // Search should not include deleted entry + Bundle search = ourRestServer.getFhirClient().search().forResource("Patient").returnBundle(Bundle.class).execute(); + ourLog.info("Search:\n{}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(search)); + assertEquals(0, search.getEntry().size()); + } @Test @@ -140,14 +144,14 @@ public class HashMapResourceProviderTest { // Create Res 1 Patient p = new Patient(); p.setActive(true); - IIdType id1 = ourClient.create().resource(p).execute().getId(); + IIdType id1 = ourRestServer.getFhirClient().create().resource(p).execute().getId(); assertThat(id1.getIdPart(), matchesPattern("[0-9]+")); assertEquals("1", id1.getVersionIdPart()); // Create Res 2 p = new Patient(); p.setActive(true); - IIdType id2 = ourClient.create().resource(p).execute().getId(); + IIdType id2 = ourRestServer.getFhirClient().create().resource(p).execute().getId(); assertThat(id2.getIdPart(), matchesPattern("[0-9]+")); assertEquals("1", id2.getVersionIdPart()); @@ -155,11 +159,11 @@ public class HashMapResourceProviderTest { p = new Patient(); p.setId(id2); p.setActive(false); - id2 = ourClient.update().resource(p).execute().getId(); + id2 = ourRestServer.getFhirClient().update().resource(p).execute().getId(); assertThat(id2.getIdPart(), matchesPattern("[0-9]+")); assertEquals("2", id2.getVersionIdPart()); - Bundle history = ourClient + Bundle history = ourRestServer.getFhirClient() .history() .onInstance(id2.toUnqualifiedVersionless()) .andReturnBundle(Bundle.class) @@ -184,14 +188,14 @@ public class HashMapResourceProviderTest { // Create Res 1 Patient p = new Patient(); p.setActive(true); - IIdType id1 = ourClient.create().resource(p).execute().getId(); + IIdType id1 = ourRestServer.getFhirClient().create().resource(p).execute().getId(); assertThat(id1.getIdPart(), matchesPattern("[0-9]+")); assertEquals("1", id1.getVersionIdPart()); // Create Res 2 p = new Patient(); p.setActive(true); - IIdType id2 = ourClient.create().resource(p).execute().getId(); + IIdType id2 = ourRestServer.getFhirClient().create().resource(p).execute().getId(); assertThat(id2.getIdPart(), matchesPattern("[0-9]+")); assertEquals("1", id2.getVersionIdPart()); @@ -199,11 +203,11 @@ public class HashMapResourceProviderTest { p = new Patient(); p.setId(id2); p.setActive(false); - id2 = ourClient.update().resource(p).execute().getId(); + id2 = ourRestServer.getFhirClient().update().resource(p).execute().getId(); assertThat(id2.getIdPart(), matchesPattern("[0-9]+")); assertEquals("2", id2.getVersionIdPart()); - Bundle history = ourClient + Bundle history = ourRestServer.getFhirClient() .history() .onType(Patient.class) .andReturnBundle(Bundle.class) @@ -228,20 +232,23 @@ public class HashMapResourceProviderTest { for (int i = 0; i < 100; i++) { Patient p = new Patient(); p.addName().setFamily("FAM" + i); - ourClient.registerInterceptor(new LoggingInterceptor(true)); - IIdType id = ourClient.create().resource(p).execute().getId(); + ourRestServer.getFhirClient().registerInterceptor(new LoggingInterceptor(true)); + IIdType id = ourRestServer.getFhirClient().create().resource(p).execute().getId(); assertThat(id.getIdPart(), matchesPattern("[0-9]+")); assertEquals("1", id.getVersionIdPart()); } // Search - Bundle resp = ourClient + Bundle resp = ourRestServer.getFhirClient() .search() .forResource("Patient") .returnBundle(Bundle.class) .execute(); + ourLog.info("Search:\n{}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(resp)); assertEquals(100, resp.getTotal()); assertEquals(100, resp.getEntry().size()); + assertFalse(resp.getEntry().get(0).hasRequest()); + assertFalse(resp.getEntry().get(1).hasRequest()); assertEquals(1, myPatientResourceProvider.getCountSearch()); @@ -253,13 +260,13 @@ public class HashMapResourceProviderTest { for (int i = 0; i < 100; i++) { Patient p = new Patient(); p.addName().setFamily("FAM" + i); - IIdType id = ourClient.create().resource(p).execute().getId(); + IIdType id = ourRestServer.getFhirClient().create().resource(p).execute().getId(); assertThat(id.getIdPart(), matchesPattern("[0-9]+")); assertEquals("1", id.getVersionIdPart()); } // Search - Bundle resp = ourClient + Bundle resp = ourRestServer.getFhirClient() .search() .forResource("Patient") .where(IAnyResource.RES_ID.exactly().codes("2", "3")) @@ -270,7 +277,7 @@ public class HashMapResourceProviderTest { assertThat(respIds, containsInAnyOrder("Patient/2", "Patient/3")); // Search - resp = ourClient + resp = ourRestServer.getFhirClient() .search() .forResource("Patient") .where(IAnyResource.RES_ID.exactly().codes("2", "3")) @@ -281,7 +288,7 @@ public class HashMapResourceProviderTest { respIds = resp.getEntry().stream().map(t -> t.getResource().getIdElement().toUnqualifiedVersionless().getValue()).collect(Collectors.toList()); assertThat(respIds, containsInAnyOrder("Patient/2", "Patient/3")); - resp = ourClient + resp = ourRestServer.getFhirClient() .search() .forResource("Patient") .where(IAnyResource.RES_ID.exactly().codes("2", "3")) @@ -299,7 +306,7 @@ public class HashMapResourceProviderTest { // Create Patient p = new Patient(); p.setActive(true); - IIdType id = ourClient.create().resource(p).execute().getId(); + IIdType id = ourRestServer.getFhirClient().create().resource(p).execute().getId(); assertThat(id.getIdPart(), matchesPattern("[0-9]+")); assertEquals("1", id.getVersionIdPart()); @@ -310,7 +317,7 @@ public class HashMapResourceProviderTest { p = new Patient(); p.setId(id); p.setActive(false); - id = ourClient.update().resource(p).execute().getId(); + id = ourRestServer.getFhirClient().update().resource(p).execute().getId(); assertThat(id.getIdPart(), matchesPattern("[0-9]+")); assertEquals("2", id.getVersionIdPart()); @@ -321,71 +328,21 @@ public class HashMapResourceProviderTest { assertEquals(1, myPatientResourceProvider.getCountUpdate()); // Read - p = (Patient) ourClient.read().resource("Patient").withId(id.withVersion("1")).execute(); - assertEquals(true, p.getActive()); - p = (Patient) ourClient.read().resource("Patient").withId(id.withVersion("2")).execute(); - assertEquals(false, p.getActive()); + p = (Patient) ourRestServer.getFhirClient().read().resource("Patient").withId(id.withVersion("1")).execute(); + assertTrue(p.getActive()); + p = (Patient) ourRestServer.getFhirClient().read().resource("Patient").withId(id.withVersion("2")).execute(); + assertFalse(p.getActive()); try { - ourClient.read().resource("Patient").withId(id.withVersion("3")).execute(); + ourRestServer.getFhirClient().read().resource("Patient").withId(id.withVersion("3")).execute(); fail(); } catch (ResourceNotFoundException e) { // good } } - private static class MyRestfulServer extends RestfulServer { - - MyRestfulServer() { - super(ourCtx); - } - - void clearData() { - for (IResourceProvider next : getResourceProviders()) { - if (next instanceof HashMapResourceProvider) { - ((HashMapResourceProvider) next).clear(); - } - } - } - - @Override - protected void initialize() throws ServletException { - super.initialize(); - - myPatientResourceProvider = new HashMapResourceProvider<>(ourCtx, Patient.class); - myObservationResourceProvider = new HashMapResourceProvider<>(ourCtx, Observation.class); - registerProvider(myPatientResourceProvider); - registerProvider(myObservationResourceProvider); - } - - - } - @AfterAll public static void afterClassClearContext() throws Exception { - JettyUtil.closeServer(ourListenerServer); TestUtil.randomizeLocaleAndTimezone(); } - @BeforeAll - public static void startListenerServer() throws Exception { - ourRestServer = new MyRestfulServer(); - - ourListenerServer = new Server(0); - - ServletContextHandler proxyHandler = new ServletContextHandler(); - proxyHandler.setContextPath("/"); - - ServletHolder servletHolder = new ServletHolder(); - servletHolder.setServlet(ourRestServer); - proxyHandler.addServlet(servletHolder, "/*"); - - ourListenerServer.setHandler(proxyHandler); - JettyUtil.startServer(ourListenerServer); - int ourListenerPort = JettyUtil.getPortForStartedServer(ourListenerServer); - String ourBase = "http://localhost:" + ourListenerPort + "/"; - ourCtx.getRestfulClientFactory().setSocketTimeout(120000); - ourClient = ourCtx.newRestfulGenericClient(ourBase); - } - - } diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/HashMapResourceProviderExtension.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/HashMapResourceProviderExtension.java index ff24f8087e6..ae513a65fdc 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/HashMapResourceProviderExtension.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/HashMapResourceProviderExtension.java @@ -76,6 +76,7 @@ public class HashMapResourceProviderExtension extends H myRestfulServerExtension.getRestfulServer().registerProvider(HashMapResourceProviderExtension.this); } + @Override public synchronized MethodOutcome update(T theResource, String theConditional, RequestDetails theRequestDetails) { T resourceClone = getFhirContext().newTerser().clone(theResource); myUpdates.add(resourceClone); diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/RestfulServerExtension.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/RestfulServerExtension.java index 5cf65bcd262..40fe36a1e35 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/RestfulServerExtension.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/RestfulServerExtension.java @@ -30,6 +30,9 @@ import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; import ca.uhn.fhir.rest.server.IPagingProvider; import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.interceptor.ResponseValidatingInterceptor; +import ca.uhn.fhir.validation.FhirValidator; +import ca.uhn.fhir.validation.ResultSeverityEnum; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.time.DateUtils; import org.junit.jupiter.api.extension.ExtensionContext; diff --git a/hapi-fhir-testpage-overlay/pom.xml b/hapi-fhir-testpage-overlay/pom.xml index 42f45d0f2b4..92298aefe31 100644 --- a/hapi-fhir-testpage-overlay/pom.xml +++ b/hapi-fhir-testpage-overlay/pom.xml @@ -1,4 +1,5 @@ - + 4.0.0 @@ -98,19 +99,6 @@ jakarta.annotation-api - - ch.qos.logback - logback-classic - test - - - - - org.apache.derby - derby - test - - org.springframework @@ -189,7 +177,12 @@ popper.js - + + + ch.qos.logback + logback-classic + test + org.eclipse.jetty jetty-servlets @@ -220,6 +213,22 @@ commons-dbcp2 test + + net.sourceforge.htmlunit + htmlunit + test + + + org.springframework + spring-test + test + + + ca.uhn.hapi.fhir + hapi-fhir-test-utilities + ${project.version} + test + @@ -235,9 +244,11 @@ - + - + @@ -262,17 +273,17 @@ - org.apache.maven.plugins - maven-source-plugin - - - package - - jar-no-fork - - - - + org.apache.maven.plugins + maven-source-plugin + + + package + + jar-no-fork + + + + diff --git a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/BaseController.java b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/BaseController.java index 6fb3dcb66b2..14d6f766281 100644 --- a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/BaseController.java +++ b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/BaseController.java @@ -12,6 +12,7 @@ import ca.uhn.fhir.rest.client.api.IHttpRequest; import ca.uhn.fhir.rest.client.api.IHttpResponse; import ca.uhn.fhir.rest.client.impl.GenericClient; import ca.uhn.fhir.to.model.HomeRequest; +import ca.uhn.fhir.util.BundleUtil; import ca.uhn.fhir.util.ExtensionConstants; import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; import org.apache.commons.io.IOUtils; @@ -21,8 +22,10 @@ import org.apache.http.Header; import org.apache.http.entity.ContentType; import org.apache.http.message.BasicHeader; import org.hl7.fhir.instance.model.api.IAnyResource; +import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseConformance; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IBaseXhtml; import org.hl7.fhir.instance.model.api.IDomainResource; import org.hl7.fhir.r5.model.CapabilityStatement; import org.springframework.beans.factory.annotation.Autowired; @@ -373,23 +376,38 @@ public class BaseController { private String parseNarrative(HomeRequest theRequest, EncodingEnum theCtEnum, String theResultBody) { try { - IBaseResource par = theCtEnum.newParser(getContext(theRequest)).parseResource(theResultBody); - String retVal; - if (par instanceof IResource) { - IResource resource = (IResource) par; - retVal = resource.getText().getDiv().getValueAsString(); - } else if (par instanceof IDomainResource) { - retVal = ((IDomainResource) par).getText().getDivAsString(); - } else { - retVal = null; - } - return StringUtils.defaultString(retVal); + FhirContext context = getContext(theRequest); + IBaseResource result = theCtEnum.newParser(context).parseResource(theResultBody); + return parseNarrative(context, result); } catch (Exception e) { ourLog.error("Failed to parse resource", e); return ""; } } + private String parseNarrative(FhirContext theContext, IBaseResource theResult) throws Exception { + String retVal = null; + if (theResult instanceof IResource) { + IResource resource = (IResource) theResult; + retVal = resource.getText().getDiv().getValueAsString(); + } else if (theResult instanceof IDomainResource) { + retVal = ((IDomainResource) theResult).getText().getDivAsString(); + } else if (theResult instanceof IBaseBundle) { + // If this is a document, we'll pull the narrative from the Composition + IBaseBundle bundle = (IBaseBundle) theResult; + if ("document".equals(BundleUtil.getBundleType(theContext, bundle))) { + IBaseResource firstResource = theContext.newTerser().getSingleValueOrNull(bundle, "Bundle.entry.resource", IBaseResource.class); + if (firstResource != null && "Composition".equals(theContext.getResourceType(firstResource))) { + IBaseXhtml html = theContext.newTerser().getSingleValueOrNull(firstResource, "text.div", IBaseXhtml.class); + if (html != null) { + retVal = html.getValueAsString(); + } + } + } + } + return StringUtils.defaultString(retVal); + } + protected String preProcessMessageBody(String theBody) { if (theBody == null) { return ""; @@ -462,7 +480,6 @@ public class BaseController { switch (ctEnum) { case JSON: if (theResultType == ResultType.RESOURCE) { - narrativeString = parseNarrative(theRequest, ctEnum, resultBody); resultDescription.append("JSON resource"); } else if (theResultType == ResultType.BUNDLE) { resultDescription.append("JSON bundle"); @@ -472,7 +489,6 @@ public class BaseController { case XML: default: if (theResultType == ResultType.RESOURCE) { - narrativeString = parseNarrative(theRequest, ctEnum, resultBody); resultDescription.append("XML resource"); } else if (theResultType == ResultType.BUNDLE) { resultDescription.append("XML bundle"); @@ -480,6 +496,7 @@ public class BaseController { } break; } + narrativeString = parseNarrative(theRequest, ctEnum, resultBody); } resultDescription.append(" (").append(defaultString(resultBody).length() + " bytes)"); @@ -508,6 +525,9 @@ public class BaseController { theModelMap.put("narrative", narrativeString); theModelMap.put("latencyMs", theLatency); + theModelMap.put("config", myConfig); + theModelMap.put("serverId", theRequest.getServerId()); + } catch (Exception e) { ourLog.error("Failure during processing", e); theModelMap.put("errorMsg", toDisplayError("Error during processing: " + e.getMessage(), e)); diff --git a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/Controller.java b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/Controller.java index 25cb24bb99f..4aa001e2b94 100644 --- a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/Controller.java +++ b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/Controller.java @@ -25,10 +25,11 @@ import ca.uhn.fhir.rest.gclient.QuantityClientParam; import ca.uhn.fhir.rest.gclient.QuantityClientParam.IAndUnits; import ca.uhn.fhir.rest.gclient.StringClientParam; import ca.uhn.fhir.rest.gclient.TokenClientParam; +import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.to.model.HomeRequest; import ca.uhn.fhir.to.model.ResourceRequest; import ca.uhn.fhir.to.model.TransactionRequest; -import ca.uhn.fhir.util.UrlUtil; +import ca.uhn.fhir.util.StopWatch; import com.google.gson.stream.JsonWriter; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.dstu3.model.CapabilityStatement; @@ -38,6 +39,7 @@ import org.hl7.fhir.dstu3.model.CapabilityStatement.CapabilityStatementRestResou import org.hl7.fhir.dstu3.model.StringType; 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.springframework.ui.ModelMap; import org.springframework.validation.BindingResult; @@ -599,6 +601,53 @@ public class Controller extends BaseController { } + + @SuppressWarnings("unchecked") + @RequestMapping(value = { "/operation" }) + public String actionOperation(final HttpServletRequest theReq, final HomeRequest theRequest, final BindingResult theBindingResult, final ModelMap theModel) { + + String instanceType = theReq.getParameter("instanceType"); + String instanceId = theReq.getParameter("instanceId"); + String operationName = theReq.getParameter("operationName"); + + boolean finished = false; + + addCommonParams(theReq, theRequest, theModel); + + CaptureInterceptor interceptor = new CaptureInterceptor(); + GenericClient client = theRequest.newClient(theReq, getContext(theRequest), myConfig, interceptor); + client.setPrettyPrint(true); + + Class type = getContext(theRequest).getResourceDefinition(instanceType).getImplementingClass(); + Class parametersType = (Class) getContext(theRequest).getResourceDefinition("Parameters").getImplementingClass(); + + StopWatch sw = new StopWatch(); + ResultType returnsResource = ResultType.BUNDLE; + try { + client + .operation() + .onInstance(instanceType + "/" + instanceId) + .named(operationName) + .withNoParameters(parametersType) + .useHttpGet() + .execute(); + } catch (DataFormatException e) { + ourLog.warn("Failed to parse resource", e); + theModel.put("errorMsg", toDisplayError("Failed to parse message body. Error was: " + e.getMessage(), e)); + finished = true; + } catch (BaseServerResponseException e) { + theModel.put("errorMsg", e.getMessage()); + returnsResource = ResultType.RESOURCE; + } + + String outcomeDescription = "Execute " + operationName + " Operation"; + processAndAddLastClientInvocation(client, returnsResource, theModel, sw.getMillis(), outcomeDescription, interceptor, theRequest); + + return "result"; + } + + + private void doActionHistory(HttpServletRequest theReq, HomeRequest theRequest, BindingResult theBindingResult, ModelMap theModel, String theMethod, String theMethodDescription) { addCommonParams(theReq, theRequest, theModel); diff --git a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/FhirTesterMvcConfig.java b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/FhirTesterMvcConfig.java index a1e8b4698e0..958b872bb72 100644 --- a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/FhirTesterMvcConfig.java +++ b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/FhirTesterMvcConfig.java @@ -8,20 +8,22 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.thymeleaf.spring5.SpringTemplateEngine; import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver; import org.thymeleaf.spring5.view.ThymeleafViewResolver; import org.thymeleaf.templatemode.TemplateMode; +import javax.annotation.Nonnull; + @Configuration @EnableWebMvc @ComponentScan(basePackages = "ca.uhn.fhir.to") -public class FhirTesterMvcConfig extends WebMvcConfigurerAdapter { +public class FhirTesterMvcConfig implements WebMvcConfigurer { @Override - public void addResourceHandlers(ResourceHandlerRegistry theRegistry) { + public void addResourceHandlers(@Nonnull ResourceHandlerRegistry theRegistry) { WebUtil.webJarAddBoostrap(theRegistry); WebUtil.webJarAddJQuery(theRegistry); WebUtil.webJarAddFontAwesome(theRegistry); @@ -40,13 +42,17 @@ public class FhirTesterMvcConfig extends WebMvcConfigurerAdapter { } @Bean - public SpringResourceTemplateResolver templateResolver() { + public SpringResourceTemplateResolver templateResolver(TesterConfig theTesterConfig) { SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver(); resolver.setPrefix("/WEB-INF/templates/"); resolver.setSuffix(".html"); resolver.setTemplateMode(TemplateMode.HTML); resolver.setCharacterEncoding("UTF-8"); + if (theTesterConfig.getDebugTemplatesMode()) { + resolver.setCacheable(false); + } + return resolver; } @@ -56,17 +62,17 @@ public class FhirTesterMvcConfig extends WebMvcConfigurerAdapter { } @Bean - public ThymeleafViewResolver viewResolver() { + public ThymeleafViewResolver viewResolver(SpringTemplateEngine theTemplateEngine) { ThymeleafViewResolver viewResolver = new ThymeleafViewResolver(); - viewResolver.setTemplateEngine(templateEngine()); + viewResolver.setTemplateEngine(theTemplateEngine); viewResolver.setCharacterEncoding("UTF-8"); return viewResolver; } @Bean - public SpringTemplateEngine templateEngine() { + public SpringTemplateEngine templateEngine(SpringResourceTemplateResolver theTemplateResolver) { SpringTemplateEngine templateEngine = new SpringTemplateEngine(); - templateEngine.setTemplateResolver(templateResolver()); + templateEngine.setTemplateResolver(theTemplateResolver); return templateEngine; } diff --git a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/TesterConfig.java b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/TesterConfig.java index 36178bf8533..145dc05a140 100644 --- a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/TesterConfig.java +++ b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/TesterConfig.java @@ -1,29 +1,35 @@ package ca.uhn.fhir.to; +import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.i18n.Msg; -import java.util.*; - -import javax.annotation.PostConstruct; - +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.server.util.ITestingUiClientFactory; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; +import org.hl7.fhir.instance.model.api.IIdType; import org.springframework.beans.factory.annotation.Required; -import ca.uhn.fhir.context.FhirVersionEnum; -import ca.uhn.fhir.rest.server.util.ITestingUiClientFactory; +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; public class TesterConfig { - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TesterConfig.class); - public static final String SYSPROP_FORCE_SERVERS = "ca.uhn.fhir.to.TesterConfig_SYSPROP_FORCE_SERVERS"; - + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TesterConfig.class); + private final LinkedHashMap myIdToAllowsApiKey = new LinkedHashMap<>(); + private final LinkedHashMap myIdToFhirVersion = new LinkedHashMap<>(); + private final LinkedHashMap myIdToServerBase = new LinkedHashMap<>(); + private final LinkedHashMap myIdToServerName = new LinkedHashMap<>(); + private final List myServerBuilders = new ArrayList<>(); + private final LinkedHashMap> myServerIdToTypeToOperationNameToInclusionChecker = new LinkedHashMap<>(); + private final LinkedHashMap> myServerIdToTypeToInteractionNameToInclusionChecker = new LinkedHashMap<>(); private ITestingUiClientFactory myClientFactory; - private LinkedHashMap myIdToAllowsApiKey = new LinkedHashMap(); - private LinkedHashMap myIdToFhirVersion = new LinkedHashMap(); - private LinkedHashMap myIdToServerBase = new LinkedHashMap(); - private LinkedHashMap myIdToServerName = new LinkedHashMap(); private boolean myRefuseToFetchThirdPartyUrls = true; - private List myServerBuilders = new ArrayList(); + private boolean myDebugTemplatesMode; public IServerBuilderStep1 addServer() { ServerBuilder retVal = new ServerBuilder(); @@ -42,6 +48,11 @@ public class TesterConfig { myIdToServerBase.put(next.myId, next.myBaseUrl); myIdToServerName.put(next.myId, next.myName); myIdToAllowsApiKey.put(next.myId, next.myAllowsApiKey); + myServerIdToTypeToOperationNameToInclusionChecker.put(next.myId, next.myOperationNameToInclusionChecker); + myServerIdToTypeToInteractionNameToInclusionChecker.put(next.myId, next.mySearchResultRowInteractionEnabled); + if (next.myEnableDebugTemplates) { + myDebugTemplatesMode = true; + } } myServerBuilders.clear(); } @@ -50,8 +61,12 @@ public class TesterConfig { return myClientFactory; } + public void setClientFactory(ITestingUiClientFactory theClientFactory) { + myClientFactory = theClientFactory; + } + public boolean getDebugTemplatesMode() { - return true; + return myDebugTemplatesMode; } public LinkedHashMap getIdToAllowsApiKey() { @@ -72,24 +87,52 @@ public class TesterConfig { /** * If set to {@literal true} (default is true) the server will refuse to load URLs in - * response payloads the refer to third party servers (e.g. paging URLs etc) + * response payloads that refer to third party servers (e.g. paging URLs etc) */ public boolean isRefuseToFetchThirdPartyUrls() { return myRefuseToFetchThirdPartyUrls; } - public void setClientFactory(ITestingUiClientFactory theClientFactory) { - myClientFactory = theClientFactory; - } - /** * If set to {@literal true} (default is true) the server will refuse to load URLs in - * response payloads the refer to third party servers (e.g. paging URLs etc) + * response payloads that refer to third party servers (e.g. paging URLs etc) */ public void setRefuseToFetchThirdPartyUrls(boolean theRefuseToFetchThirdPartyUrls) { myRefuseToFetchThirdPartyUrls = theRefuseToFetchThirdPartyUrls; } + /** + * Called from Thymeleaf + */ + @SuppressWarnings("unused") + public List getSearchResultRowOperations(String theId, IIdType theResourceId) { + List retVal = new ArrayList<>(); + + Map operationNamesToInclusionCheckers = myServerIdToTypeToOperationNameToInclusionChecker.get(theId); + for (String operationName : operationNamesToInclusionCheckers.keySet()) { + IInclusionChecker checker = operationNamesToInclusionCheckers.get(operationName); + if (checker.shouldInclude(theResourceId)) { + retVal.add(operationName); + } + } + + return retVal; + } + + /** + * Called from Thymeleaf + */ + @SuppressWarnings("unused") + public boolean isSearchResultRowInteractionEnabled(String theServerId, String theInteractionName, IIdType theResourceId) { + List retVal = new ArrayList<>(); + + Map interactionNamesToInclusionCheckers = myServerIdToTypeToInteractionNameToInclusionChecker.get(theServerId); + RestOperationTypeEnum interaction = RestOperationTypeEnum.forCode(theInteractionName); + Validate.isTrue(interaction != null, "Unknown interaction: %s", theInteractionName); + IInclusionChecker inclusionChecker = interactionNamesToInclusionCheckers.getOrDefault(interaction, id -> false); + return inclusionChecker.shouldInclude(theResourceId); + } + @Required public void setServers(List theServers) { List servers = theServers; @@ -97,7 +140,7 @@ public class TesterConfig { // This is mostly for unit tests String force = System.getProperty(SYSPROP_FORCE_SERVERS); if (StringUtils.isNotBlank(force)) { - ourLog.warn("Forcing server confirguration because of system property: {}", force); + ourLog.warn("Forcing server configuration because of system property: {}", force); servers = Collections.singletonList(force); } @@ -148,15 +191,49 @@ public class TesterConfig { IServerBuilderStep5 allowsApiKey(); + /** + * If this is set, Thymeleaf UI templates will be run in debug mode, meaning + * no caching between executions. This is helpful if you want to make live changes + * to the template. + * + * @since 6.4.0 + */ + IServerBuilderStep5 enableDebugTemplates(); + + /** + * Use this method to add buttons to invoke operations on the search result table. + */ + ServerBuilder withSearchResultRowOperation(String theOperationName, IInclusionChecker theInclusionChecker); + + /** + * Use this method to enable/disable the interaction buttons on the search result rows table. + * By default {@link RestOperationTypeEnum#READ} and {@link RestOperationTypeEnum#UPDATE} are + * already enabled, and they are currently the only interactions supported. + */ + ServerBuilder withSearchResultRowInteraction(RestOperationTypeEnum theInteraction, IInclusionChecker theEnabled); + } + + public interface IInclusionChecker { + + boolean shouldInclude(IIdType theResourceId); + } public class ServerBuilder implements IServerBuilderStep1, IServerBuilderStep2, IServerBuilderStep3, IServerBuilderStep4, IServerBuilderStep5 { + private final Map myOperationNameToInclusionChecker = new LinkedHashMap<>(); + private final Map mySearchResultRowInteractionEnabled = new LinkedHashMap<>(); private boolean myAllowsApiKey; private String myBaseUrl; private String myId; private String myName; private FhirVersionEnum myVersion; + private boolean myEnableDebugTemplates; + + public ServerBuilder() { + mySearchResultRowInteractionEnabled.put(RestOperationTypeEnum.READ, id -> true); + mySearchResultRowInteractionEnabled.put(RestOperationTypeEnum.UPDATE, id -> true); + } @Override public IServerBuilderStep1 addServer() { @@ -171,6 +248,24 @@ public class TesterConfig { return this; } + @Override + public IServerBuilderStep5 enableDebugTemplates() { + myEnableDebugTemplates = true; + return this; + } + + @Override + public ServerBuilder withSearchResultRowOperation(String theOperationName, IInclusionChecker theResourceType) { + myOperationNameToInclusionChecker.put(theOperationName, theResourceType); + return this; + } + + @Override + public ServerBuilder withSearchResultRowInteraction(RestOperationTypeEnum theInteraction, IInclusionChecker theEnabled) { + mySearchResultRowInteractionEnabled.put(theInteraction, theEnabled); + return this; + } + @Override public IServerBuilderStep4 withBaseUrl(String theBaseUrl) { Validate.notBlank(theBaseUrl, "theBaseUrl can not be blank"); @@ -200,5 +295,4 @@ public class TesterConfig { } } - } diff --git a/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/templates/result.html b/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/templates/result.html index 7db8a0ed211..55f03862fac 100644 --- a/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/templates/result.html +++ b/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/templates/result.html @@ -117,246 +117,29 @@ + + Result Narrative + + Result Body - - -
-
-
-
- Bundle contains no entries - - - - - - - - - - - - - - -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - -
IDTitleUpdated
- - - - - - - - -
-
-
- + + + + + + + +
+
+ Payload
- - - - -
-
-
-
- Bundle contains no entries - - - - - - - - - - - - - - - -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
IDUpdated
- - - - - - -
-
-
- -
-
- - - -
-

- Raw Message -

-
@@ -367,10 +150,6 @@
- - Result Narrative - - diff --git a/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/templates/tmpl-result-controltable-hapi.html b/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/templates/tmpl-result-controltable-hapi.html new file mode 100644 index 00000000000..1128f920448 --- /dev/null +++ b/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/templates/tmpl-result-controltable-hapi.html @@ -0,0 +1,112 @@ + + +
+
+
+
+ Bundle contains no entries + + + + + + + + + + + + + + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
IDTitleUpdated
+ + + + + + + + +
+
+
+ +
+
+ +
diff --git a/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/templates/tmpl-result-controltable-ri.html b/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/templates/tmpl-result-controltable-ri.html new file mode 100644 index 00000000000..f9097077d8e --- /dev/null +++ b/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/templates/tmpl-result-controltable-ri.html @@ -0,0 +1,105 @@ + + +
+
+
+
+ +
+ Bundle contains no entries + + + + +
+ + + + + + + + + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
IDUpdated
+ + + + + + + + + + +
+
+
+
+
+ +
diff --git a/hapi-fhir-testpage-overlay/src/main/webapp/css/tester.css b/hapi-fhir-testpage-overlay/src/main/webapp/css/tester.css index 3989fb4fe87..e5f52ad246c 100644 --- a/hapi-fhir-testpage-overlay/src/main/webapp/css/tester.css +++ b/hapi-fhir-testpage-overlay/src/main/webapp/css/tester.css @@ -26,6 +26,19 @@ body { font-family: sans-serif; } +H1 { + font-size: 1.3em; +} +H2 { + font-size: 1.2em; +} +H3 { + font-size: 1.1em; +} +H4 { + font-size: 1.1em; +} + .clientCodeBox { font-family: monospace; font-size: 0.8em; @@ -53,6 +66,14 @@ label { line-height: 0.8em; } +TD.resultControlButtons { + font-size: 0; +} + +TD.resultControlButtons BUTTON { + margin: 2px; +} + TD.headerBox { line-height: 0.8em !important; } @@ -153,10 +174,12 @@ DIV.navbarBreadcrumb:HOVER, A.navbarBreadcrumb:HOVER { } DIV.resultBodyActual { - /* - max-height: 400px; - overflow: scroll; - */ + padding: 5px; +} + +DIV.panel-title-text { + font-weight: bold; + margin: 12px; } PRE.resultBodyPre { diff --git a/hapi-fhir-testpage-overlay/src/main/webapp/js/RestfulTester.js b/hapi-fhir-testpage-overlay/src/main/webapp/js/RestfulTester.js index 646bf4cdd7c..0d225c66bf0 100644 --- a/hapi-fhir-testpage-overlay/src/main/webapp/js/RestfulTester.js +++ b/hapi-fhir-testpage-overlay/src/main/webapp/js/RestfulTester.js @@ -507,8 +507,20 @@ function updateFromEntriesTable(source, type, id, vid) { } setResource(btn, type); $("#outerForm").attr("action", "resource").submit(); -} +} +/* + * Handler for Operation button which appears on each entry in the + * summary at the top of a Bundle response view + */ +function executeOperation(source, type, id, operationName) { + var btn = $(source); + btn.button('loading'); + btn.append($('', { type: 'hidden', name: 'instanceType', value: type })); + btn.append($('', { type: 'hidden', name: 'instanceId', value: id })); + btn.append($('', { type: 'hidden', name: 'operationName', value: operationName })); + $("#outerForm").attr("action", "operation").submit(); +} /** * http://stackoverflow.com/a/10997390/11236 diff --git a/hapi-fhir-testpage-overlay/src/test/java/ca/uhn/fhir/jpa/test/FhirTesterConfig.java b/hapi-fhir-testpage-overlay/src/test/java/ca/uhn/fhir/jpa/test/FhirTesterConfig.java index 8df0cd3f1f1..658a60ce83f 100644 --- a/hapi-fhir-testpage-overlay/src/test/java/ca/uhn/fhir/jpa/test/FhirTesterConfig.java +++ b/hapi-fhir-testpage-overlay/src/test/java/ca/uhn/fhir/jpa/test/FhirTesterConfig.java @@ -8,7 +8,6 @@ import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.to.FhirTesterMvcConfig; import ca.uhn.fhir.to.TesterConfig; -//@formatter:off /** * This spring config file configures the web testing module. It serves two * purposes: @@ -24,15 +23,15 @@ public class FhirTesterConfig { /** * This bean tells the testing webpage which servers it should configure itself * to communicate with. In this example we configure it to talk to the local - * server, as well as one public server. If you are creating a project to - * deploy somewhere else, you might choose to only put your own server's + * server, as well as one public server. If you are creating a project to + * deploy somewhere else, you might choose to only put your own server's * address here. - * + * * Note the use of the ${serverBase} variable below. This will be replaced with * the base URL as reported by the server itself. Often for a simple Tomcat * (or other container) installation, this will end up being something * like "http://localhost:8080/hapi-fhir-jpaserver-example". If you are - * deploying your server to a place with a fully qualified domain name, + * deploying your server to a place with a fully qualified domain name, * you might want to use that instead of using the variable. */ @Bean @@ -41,7 +40,7 @@ public class FhirTesterConfig { retVal .addServer() .withId("internal") - .withFhirVersion(FhirVersionEnum.DSTU2) + .withFhirVersion(FhirVersionEnum.R4) .withBaseUrl("http://localhost:8888/fhir") .withName("Localhost Server") .allowsApiKey() @@ -63,6 +62,5 @@ public class FhirTesterConfig { .withName("Local Tester"); return retVal; } - + } -//@formatter:on diff --git a/hapi-fhir-testpage-overlay/src/test/java/ca/uhn/fhir/jpa/test/WebTest.java b/hapi-fhir-testpage-overlay/src/test/java/ca/uhn/fhir/jpa/test/WebTest.java new file mode 100644 index 00000000000..1d278b0533a --- /dev/null +++ b/hapi-fhir-testpage-overlay/src/test/java/ca/uhn/fhir/jpa/test/WebTest.java @@ -0,0 +1,271 @@ +package ca.uhn.fhir.jpa.test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.Validate; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.test.utilities.JettyUtil; +import ca.uhn.fhir.test.utilities.server.HashMapResourceProviderExtension; +import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; +import com.gargoylesoftware.css.parser.CSSErrorHandler; +import com.gargoylesoftware.htmlunit.Page; +import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.html.HtmlAnchor; +import com.gargoylesoftware.htmlunit.html.HtmlButton; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.gargoylesoftware.htmlunit.html.HtmlTable; +import com.gargoylesoftware.htmlunit.html.HtmlTableCell; +import com.gargoylesoftware.htmlunit.html.HtmlTableRow; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Composition; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.InstantType; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Patient; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.htmlunit.MockMvcWebConnection; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.servlet.DispatcherServlet; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class WebTest { + private static final Logger ourLog = LoggerFactory.getLogger(WebTest.class); + private static final FhirContext ourCtx = FhirContext.forR4Cached(); + @RegisterExtension + @Order(0) + private static final RestfulServerExtension ourFhirServer = new RestfulServerExtension(ourCtx) + .registerProvider(new MyPatientFakeDocumentController()); + @RegisterExtension + @Order(1) + private static final HashMapResourceProviderExtension ourPatientProvider = new HashMapResourceProviderExtension<>(ourFhirServer, Patient.class); + protected static MockMvc ourMockMvc; + private static Server ourOverlayServer; + private WebClient myWebClient; + + @BeforeEach + public void before() throws Exception { + if (ourOverlayServer == null) { + AnnotationConfigWebApplicationContext appCtx = new AnnotationConfigWebApplicationContext(); + appCtx.register(WebTestFhirTesterConfig.class); + + DispatcherServlet dispatcherServlet = new DispatcherServlet(appCtx); + + ServletHolder holder = new ServletHolder(dispatcherServlet); + holder.setName("servlet"); + + ServletHandler servletHandler = new ServletHandler(); + servletHandler.addServletWithMapping(holder, "/*"); + + ServletContextHandler contextHandler = new MyServletContextHandler(); + contextHandler.setAllowNullPathInfo(true); + contextHandler.setServletHandler(servletHandler); + contextHandler.setResourceBase("hapi-fhir-testpage-overlay/src/main/webapp"); + + ourOverlayServer = new Server(0); + ourOverlayServer.setHandler(contextHandler); + ourOverlayServer.start(); + + ourMockMvc = MockMvcBuilders.webAppContextSetup(appCtx).build(); + } + + myWebClient = new WebClient(); + myWebClient.setWebConnection(new MockMvcWebConnection(ourMockMvc, myWebClient)); + myWebClient.getOptions().setJavaScriptEnabled(true); + myWebClient.getOptions().setCssEnabled(false); + CSSErrorHandler errorHandler = new SilentCssErrorHandler(); + myWebClient.setCssErrorHandler(errorHandler); + + ourLog.info("Started FHIR endpoint at " + ourFhirServer.getBaseUrl()); + WebTestFhirTesterConfig.setBaseUrl(ourFhirServer.getBaseUrl()); + + String baseUrl = "http://localhost:" + JettyUtil.getPortForStartedServer(ourOverlayServer) + "/"; + ourLog.info("Started test overlay at " + baseUrl); + } + + @Test + public void testSearchForPatients() throws IOException { + register5Patients(); + + // Load home page + HtmlPage page = myWebClient.getPage("http://localhost/"); + // Navigate to Patient resource page + HtmlAnchor patientLink = page.getHtmlElementById("leftResourcePatient"); + HtmlPage patientPage = patientLink.click(); + // Click search button + HtmlButton searchButton = patientPage.getHtmlElementById("search-btn"); + HtmlPage searchResultPage = searchButton.click(); + HtmlTable controlsTable = searchResultPage.getHtmlElementById("resultControlsTable"); + List controlRows = controlsTable.getBodies().get(0).getRows(); + assertEquals(5, controlRows.size()); + assertEquals("Read Update $summary $validate", controlRows.get(0).getCell(0).asNormalizedText()); + assertEquals("Patient/A0/_history/1", controlRows.get(0).getCell(1).asNormalizedText()); + assertEquals("Patient/A4/_history/1", controlRows.get(4).getCell(1).asNormalizedText()); + } + + @Test + public void testHistoryWithDeleted() throws IOException { + register5Patients(); + for (int i = 0; i < 5; i++) { + ourFhirServer.getFhirClient().delete().resourceById(new IdType("Patient/A" + i)); + } + + + // Load home page + HtmlPage page = myWebClient.getPage("http://localhost/"); + // Navigate to Patient resource page + HtmlAnchor patientLink = page.getHtmlElementById("leftResourcePatient"); + HtmlPage patientPage = patientLink.click(); + // Click search button + HtmlButton historyButton = patientPage.getElementByName("action-history-type"); + HtmlPage searchResultPage = historyButton.click(); + HtmlTable controlsTable = searchResultPage.getHtmlElementById("resultControlsTable"); + List controlRows = controlsTable.getBodies().get(0).getRows(); + assertEquals(5, controlRows.size()); + ourLog.info(controlRows.get(0).asXml()); + assertEquals("Patient/A4/_history/1", controlRows.get(0).getCell(1).asNormalizedText()); + assertEquals("Patient/A0/_history/1", controlRows.get(4).getCell(1).asNormalizedText()); + } + + @Test + public void testInvokeCustomOperation() throws IOException { + register5Patients(); + + HtmlPage searchResultPage = searchForPatients(); + HtmlTable controlsTable = searchResultPage.getHtmlElementById("resultControlsTable"); + List controlRows = controlsTable.getBodies().get(0).getRows(); + HtmlTableCell controlsCell = controlRows.get(0).getCell(0); + + // Find the $summary button and click it + HtmlPage summaryPage = controlsCell + .getElementsByTagName("button") + .stream() + .filter(t -> t.asNormalizedText().equals("$summary")) + .findFirst() + .orElseThrow() + .click(); + + assertThat(summaryPage.asNormalizedText(), containsString("Result Narrative\t\nHELLO WORLD DOCUMENT")); + } + + @Test + public void testInvokeCustomOperation_Validate() throws IOException { + register5Patients(); + + HtmlPage searchResultPage = searchForPatients(); + HtmlTable controlsTable = searchResultPage.getHtmlElementById("resultControlsTable"); + List controlRows = controlsTable.getBodies().get(0).getRows(); + HtmlTableCell controlsCell = controlRows.get(0).getCell(0); + + // Find the $summary button and click it + HtmlPage summaryPage = controlsCell + .getElementsByTagName("button") + .stream() + .filter(t -> t.asNormalizedText().equals("$validate")) + .findFirst() + .orElseThrow() + .click(); + + assertThat(summaryPage.asNormalizedText(), containsString("\"diagnostics\": \"VALIDATION FAILURE\"")); + } + + private HtmlPage searchForPatients() throws IOException { + // Load home page + HtmlPage page = myWebClient.getPage("http://localhost/"); + // Navigate to Patient resource page + HtmlPage patientPage = page.getHtmlElementById("leftResourcePatient").click(); + // Click search button + HtmlPage searchResultPage = patientPage.getHtmlElementById("search-btn").click(); + return searchResultPage; + } + + + private void register5Patients() { + for (int i = 0; i < 5; i++) { + Patient p = new Patient(); + p.setId("Patient/A" + i); + p.getMeta().setLastUpdatedElement(new InstantType("2022-01-01T12:12:12.000Z")); + p.setActive(true); + ourPatientProvider.store(p); + } + } + + private static class MyPatientFakeDocumentController { + + @Operation(name = "summary", typeName = "Patient", idempotent = true) + public Bundle summary(@IdParam IIdType theId) { + Composition composition = new Composition(); + composition.getText().setDivAsString("
HELLO WORLD DOCUMENT
"); + + Bundle retVal = new Bundle(); + retVal.setType(Bundle.BundleType.DOCUMENT); + retVal.addEntry().setResource(composition); + + return retVal; + } + + @Operation(name = "validate", typeName = "Patient", idempotent = true) + public OperationOutcome validate(@IdParam IIdType theId) { + OperationOutcome oo = new OperationOutcome(); + oo.addIssue() + .setDiagnostics("VALIDATION FAILURE"); + throw new PreconditionFailedException("failure", oo); + } + + } + + private static class MyServletContextHandler extends ServletContextHandler { + + public MyServletContextHandler() { + super(); + _scontext = new ContextHandler.Context() { + @Override + public URL getResource(String thePath) { + File parent = new File("hapi-fhir-testpage-overlay/src/main/webapp").getAbsoluteFile(); + if (!parent.exists()) { + parent = new File("src/main/webapp").getAbsoluteFile(); + } + File file = new File(parent, thePath); + try { + return file.toURI().toURL(); + } catch (MalformedURLException e) { + throw new InternalErrorException(e); + } + } + }; + } + } + + @AfterAll + public static void afterAll() throws Exception { + JettyUtil.closeServer(ourOverlayServer); + } + + +} diff --git a/hapi-fhir-testpage-overlay/src/test/java/ca/uhn/fhir/jpa/test/WebTestFhirTesterConfig.java b/hapi-fhir-testpage-overlay/src/test/java/ca/uhn/fhir/jpa/test/WebTestFhirTesterConfig.java new file mode 100644 index 00000000000..b49b2367f6e --- /dev/null +++ b/hapi-fhir-testpage-overlay/src/test/java/ca/uhn/fhir/jpa/test/WebTestFhirTesterConfig.java @@ -0,0 +1,43 @@ +package ca.uhn.fhir.jpa.test; + +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.rest.server.util.ITestingUiClientFactory; +import ca.uhn.fhir.to.FhirTesterMvcConfig; +import ca.uhn.fhir.to.TesterConfig; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import(FhirTesterMvcConfig.class) +public class WebTestFhirTesterConfig { + + private static String ourBaseUrl; + + @Bean + public ITestingUiClientFactory clientFactory() { + // Replace the base URL + return (theFhirContext, theRequest, theServerBaseUrl) -> theFhirContext.newRestfulGenericClient(ourBaseUrl); + } + + @Bean + public TesterConfig testerConfig(ITestingUiClientFactory theClientFactory) { + TesterConfig retVal = new TesterConfig(); + retVal.setClientFactory(theClientFactory); + retVal + .addServer() + .withId("internal") + .withFhirVersion(FhirVersionEnum.R4) + .withBaseUrl("http://localhost:8000") + .withName("Localhost Server") + .withSearchResultRowOperation("$summary", id -> "Patient".equals(id.getResourceType())) + .withSearchResultRowOperation("$validate", id -> true) + .enableDebugTemplates(); + return retVal; + } + + public static void setBaseUrl(String theBaseUrl) { + ourBaseUrl = theBaseUrl; + } + +} diff --git a/hapi-fhir-testpage-overlay/src/test/resources/mvc-test-web.xml b/hapi-fhir-testpage-overlay/src/test/resources/mvc-test-web.xml new file mode 100644 index 00000000000..7014c1c2928 --- /dev/null +++ b/hapi-fhir-testpage-overlay/src/test/resources/mvc-test-web.xml @@ -0,0 +1,48 @@ + + + + + org.springframework.web.context.ContextLoaderListener + + + contextClass + + org.springframework.web.context.support.AnnotationConfigWebApplicationContext + + + + contextConfigLocation + + ca.uhn.fhir.jpa.test.FhirServerConfig + + + + + spring + org.springframework.web.servlet.DispatcherServlet + + contextClass + org.springframework.web.context.support.AnnotationConfigWebApplicationContext + + + contextConfigLocation + ca.uhn.fhir.jpa.test.FhirTesterConfig + + 2 + + + + spring + / + + + + + *.jsp + true + + + + \ No newline at end of file