Web Admin Console Improvements (#4456)
* Testpage improvements * Work on tests * Split fragment out * Reformat command buttons * Work on web admin console * Work on testpage * Fixes * Fix * Test fixes * Improve examples * Test fixes * Address review comment
This commit is contained in:
parent
456cc81b32
commit
d46f6d635e
|
@ -167,7 +167,7 @@ public class FhirValidator {
|
||||||
*/
|
*/
|
||||||
public synchronized FhirValidator registerValidatorModule(IValidatorModule theValidator) {
|
public synchronized FhirValidator registerValidatorModule(IValidatorModule theValidator) {
|
||||||
Validate.notNull(theValidator, "theValidator must not be null");
|
Validate.notNull(theValidator, "theValidator must not be null");
|
||||||
ArrayList<IValidatorModule> newValidators = new ArrayList<IValidatorModule>(myValidators.size() + 1);
|
ArrayList<IValidatorModule> newValidators = new ArrayList<>(myValidators.size() + 1);
|
||||||
newValidators.addAll(myValidators);
|
newValidators.addAll(myValidators);
|
||||||
newValidators.add(theValidator);
|
newValidators.add(theValidator);
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<tr th:each="issue : ${resource.issue}">
|
<tr th:each="issue : ${resource.issue}">
|
||||||
<td th:text="${issue.severityElement.value}" style="font-weight: bold;"></td>
|
<td th:text="${issue.severityElement.value}" style="font-weight: bold;"></td>
|
||||||
<td th:text="${issue.location}"></td>
|
<td th:text="${issue.location}"></td>
|
||||||
<td><pre th:text="${issue.diagnostics}"/></td>
|
<td th:text="${issue.diagnostics}"></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -21,6 +21,7 @@ package ca.uhn.hapi.fhir.docs;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import ca.uhn.fhir.context.FhirVersionEnum;
|
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||||
|
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
||||||
import ca.uhn.fhir.to.FhirTesterMvcConfig;
|
import ca.uhn.fhir.to.FhirTesterMvcConfig;
|
||||||
import ca.uhn.fhir.to.TesterConfig;
|
import ca.uhn.fhir.to.TesterConfig;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
@ -60,15 +61,21 @@ public class FhirTesterConfig {
|
||||||
retVal
|
retVal
|
||||||
.addServer()
|
.addServer()
|
||||||
.withId("home")
|
.withId("home")
|
||||||
.withFhirVersion(FhirVersionEnum.DSTU2)
|
.withFhirVersion(FhirVersionEnum.R4)
|
||||||
.withBaseUrl("${serverBase}/fhir")
|
.withBaseUrl("${serverBase}/fhir")
|
||||||
.withName("Local Tester")
|
.withName("Local Tester")
|
||||||
|
// Add a $diff button on search result rows where version > 1
|
||||||
|
.withSearchResultRowOperation("$diff", id -> id.isVersionIdPartValidLong() && id.getVersionIdPartAsLong() > 1)
|
||||||
|
|
||||||
.addServer()
|
.addServer()
|
||||||
.withId("hapi")
|
.withId("hapi")
|
||||||
.withFhirVersion(FhirVersionEnum.DSTU2)
|
.withFhirVersion(FhirVersionEnum.R4)
|
||||||
.withBaseUrl("http://fhirtest.uhn.ca/baseDstu2")
|
.withBaseUrl("http://hapi.fhir.org/baseR4")
|
||||||
.withName("Public HAPI Test Server");
|
.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
|
* Use the method below to supply a client "factory" which can be used
|
||||||
* if your server requires authentication
|
* if your server requires authentication
|
||||||
|
|
|
@ -23,8 +23,10 @@ package ca.uhn.hapi.fhir.docs;
|
||||||
import ca.uhn.fhir.i18n.Msg;
|
import ca.uhn.fhir.i18n.Msg;
|
||||||
import ca.uhn.fhir.context.FhirContext;
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
import ca.uhn.fhir.model.api.Include;
|
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.TemporalPrecisionEnum;
|
||||||
import ca.uhn.fhir.model.api.annotation.Description;
|
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.parser.DataFormatException;
|
||||||
import ca.uhn.fhir.rest.annotation.Count;
|
import ca.uhn.fhir.rest.annotation.Count;
|
||||||
import ca.uhn.fhir.rest.annotation.*;
|
import ca.uhn.fhir.rest.annotation.*;
|
||||||
|
@ -392,17 +394,33 @@ public List<Patient> getPatientHistory(
|
||||||
List<Patient> retVal = new ArrayList<Patient>();
|
List<Patient> retVal = new ArrayList<Patient>();
|
||||||
|
|
||||||
Patient patient = new Patient();
|
Patient patient = new Patient();
|
||||||
patient.addName().setFamily("Smith");
|
|
||||||
|
|
||||||
// Set the ID and version
|
// Set the ID and version
|
||||||
patient.setId(theId.withVersion("1"));
|
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;
|
return retVal;
|
||||||
}
|
}
|
||||||
//END SNIPPET: history
|
//END SNIPPET: history
|
||||||
|
|
||||||
|
|
||||||
|
private boolean isDeleted(Patient thePatient) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//START SNIPPET: vread
|
//START SNIPPET: vread
|
||||||
@Read(version=true)
|
@Read(version=true)
|
||||||
public Patient readOrVread(@IdParam IdType theId) {
|
public Patient readOrVread(@IdParam IdType theId) {
|
||||||
|
|
|
@ -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."
|
|
@ -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."
|
|
@ -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."
|
|
@ -4749,7 +4749,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
|
||||||
ourLog.info(resp);
|
ourLog.info(resp);
|
||||||
assertEquals(200, response.getStatusLine().getStatusCode());
|
assertEquals(200, response.getStatusLine().getStatusCode());
|
||||||
assertThat(resp, not(containsString("Resource has no id")));
|
assertThat(resp, not(containsString("Resource has no id")));
|
||||||
assertThat(resp, containsString("<pre>No issues detected during validation</pre>"));
|
assertThat(resp, containsString("<td>No issues detected during validation</td>"));
|
||||||
assertThat(resp,
|
assertThat(resp,
|
||||||
stringContainsInOrder("<issue>", "<severity value=\"information\"/>", "<code value=\"informational\"/>", "<diagnostics value=\"No issues detected during validation\"/>",
|
stringContainsInOrder("<issue>", "<severity value=\"information\"/>", "<code value=\"informational\"/>", "<diagnostics value=\"No issues detected during validation\"/>",
|
||||||
"</issue>"));
|
"</issue>"));
|
||||||
|
@ -4776,7 +4776,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
|
||||||
ourLog.info(resp);
|
ourLog.info(resp);
|
||||||
assertEquals(200, response.getStatusLine().getStatusCode());
|
assertEquals(200, response.getStatusLine().getStatusCode());
|
||||||
assertThat(resp, not(containsString("Resource has no id")));
|
assertThat(resp, not(containsString("Resource has no id")));
|
||||||
assertThat(resp, containsString("<pre>No issues detected during validation</pre>"));
|
assertThat(resp, containsString("<td>No issues detected during validation</td>"));
|
||||||
assertThat(resp,
|
assertThat(resp,
|
||||||
stringContainsInOrder("<issue>", "<severity value=\"information\"/>", "<code value=\"informational\"/>", "<diagnostics value=\"No issues detected during validation\"/>",
|
stringContainsInOrder("<issue>", "<severity value=\"information\"/>", "<code value=\"informational\"/>", "<diagnostics value=\"No issues detected during validation\"/>",
|
||||||
"</issue>"));
|
"</issue>"));
|
||||||
|
|
|
@ -7011,7 +7011,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
|
||||||
ourLog.info(resp);
|
ourLog.info(resp);
|
||||||
assertEquals(200, response.getStatusLine().getStatusCode());
|
assertEquals(200, response.getStatusLine().getStatusCode());
|
||||||
assertThat(resp, not(containsString("Resource has no id")));
|
assertThat(resp, not(containsString("Resource has no id")));
|
||||||
assertThat(resp, containsString("<pre>No issues detected during validation</pre>"));
|
assertThat(resp, containsString("<td>No issues detected during validation</td>"));
|
||||||
assertThat(resp,
|
assertThat(resp,
|
||||||
stringContainsInOrder("<issue>", "<severity value=\"information\"/>", "<code value=\"informational\"/>", "<diagnostics value=\"No issues detected during validation\"/>",
|
stringContainsInOrder("<issue>", "<severity value=\"information\"/>", "<code value=\"informational\"/>", "<diagnostics value=\"No issues detected during validation\"/>",
|
||||||
"</issue>"));
|
"</issue>"));
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.jpa.subscription.submit.config.SubscriptionSubmitterConfig;
|
||||||
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
|
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
|
||||||
import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor;
|
import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor;
|
||||||
|
import ca.uhn.fhirtest.ScheduledSubscriptionDeleter;
|
||||||
import ca.uhn.fhirtest.interceptor.AnalyticsInterceptor;
|
import ca.uhn.fhirtest.interceptor.AnalyticsInterceptor;
|
||||||
import ca.uhn.fhirtest.joke.HolyFooCowInterceptor;
|
import ca.uhn.fhirtest.joke.HolyFooCowInterceptor;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
@ -99,4 +100,9 @@ public class CommonConfig {
|
||||||
return "true".equalsIgnoreCase(System.getProperty("testmode.local"));
|
return "true".equalsIgnoreCase(System.getProperty("testmode.local"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ScheduledSubscriptionDeleter scheduledSubscriptionDeleter() {
|
||||||
|
return new ScheduledSubscriptionDeleter();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,24 @@
|
||||||
package ca.uhn.fhirtest.config;
|
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.context.FhirVersionEnum;
|
||||||
import ca.uhn.fhir.to.FhirTesterMvcConfig;
|
import ca.uhn.fhir.to.FhirTesterMvcConfig;
|
||||||
import ca.uhn.fhir.to.TesterConfig;
|
import ca.uhn.fhir.to.TesterConfig;
|
||||||
import ca.uhn.fhirtest.mvc.SubscriptionPlaygroundController;
|
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
|
* This spring config file configures the web testing module. It serves two
|
||||||
* purposes:
|
* purposes:
|
||||||
* 1. It imports FhirTesterMvcConfig, which is the spring config for the
|
* 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()
|
* 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
|
@Configuration
|
||||||
@Import(FhirTesterMvcConfig.class)
|
@Import(FhirTesterMvcConfig.class)
|
||||||
public class FhirTesterConfig {
|
public class FhirTesterConfig {
|
||||||
|
@ -27,15 +26,15 @@ public class FhirTesterConfig {
|
||||||
/**
|
/**
|
||||||
* This bean tells the testing webpage which servers it should configure itself
|
* 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
|
* 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
|
* 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
|
* deploy somewhere else, you might choose to only put your own server's
|
||||||
* address here.
|
* address here.
|
||||||
*
|
* <p>
|
||||||
* Note the use of the ${serverBase} variable below. This will be replaced with
|
* 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
|
* the base URL as reported by the server itself. Often for a simple Tomcat
|
||||||
* (or other container) installation, this will end up being something
|
* (or other container) installation, this will end up being something
|
||||||
* like "http://localhost:8080/hapi-fhir-jpaserver-example". If you are
|
* 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.
|
* you might want to use that instead of using the variable.
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
|
@ -43,70 +42,81 @@ public class FhirTesterConfig {
|
||||||
TesterConfig retVal = new TesterConfig();
|
TesterConfig retVal = new TesterConfig();
|
||||||
retVal
|
retVal
|
||||||
.addServer()
|
.addServer()
|
||||||
.withId("home_r4")
|
.withId("home_r4")
|
||||||
.withFhirVersion(FhirVersionEnum.R4)
|
.withFhirVersion(FhirVersionEnum.R4)
|
||||||
.withBaseUrl("http://hapi.fhir.org/baseR4")
|
.withBaseUrl("http://hapi.fhir.org/baseR4")
|
||||||
.withName("HAPI Test Server (R4 FHIR)")
|
.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()
|
.addServer()
|
||||||
.withId("home_r4b")
|
.withId("home_r4b")
|
||||||
.withFhirVersion(FhirVersionEnum.R4B)
|
.withFhirVersion(FhirVersionEnum.R4B)
|
||||||
.withBaseUrl("http://hapi.fhir.org/baseR4B")
|
.withBaseUrl("http://hapi.fhir.org/baseR4B")
|
||||||
.withName("HAPI Test Server (R4B FHIR)")
|
.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()
|
.addServer()
|
||||||
.withId("home_21")
|
.withId("home_21")
|
||||||
.withFhirVersion(FhirVersionEnum.DSTU3)
|
.withFhirVersion(FhirVersionEnum.DSTU3)
|
||||||
.withBaseUrl("http://hapi.fhir.org/baseDstu3")
|
.withBaseUrl("http://hapi.fhir.org/baseDstu3")
|
||||||
.withName("HAPI Test Server (STU3 FHIR)")
|
.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()
|
.addServer()
|
||||||
.withId("hapi_dev")
|
.withId("hapi_dev")
|
||||||
.withFhirVersion(FhirVersionEnum.DSTU2)
|
.withFhirVersion(FhirVersionEnum.DSTU2)
|
||||||
.withBaseUrl("http://hapi.fhir.org/baseDstu2")
|
.withBaseUrl("http://hapi.fhir.org/baseDstu2")
|
||||||
.withName("HAPI Test Server (DSTU2 FHIR)")
|
.withName("HAPI Test Server (DSTU2 FHIR)")
|
||||||
|
.withSearchResultRowOperation(EXTOP_VALIDATE, id -> true)
|
||||||
|
.withSearchResultRowOperation("$everything", id -> "Patient".equals(id.getResourceType()))
|
||||||
|
|
||||||
.addServer()
|
.addServer()
|
||||||
.withId("home_r5")
|
.withId("home_r5")
|
||||||
.withFhirVersion(FhirVersionEnum.R5)
|
.withFhirVersion(FhirVersionEnum.R5)
|
||||||
.withBaseUrl("http://hapi.fhir.org/baseR5")
|
.withBaseUrl("http://hapi.fhir.org/baseR5")
|
||||||
.withName("HAPI Test Server (R5 FHIR)")
|
.withName("HAPI Test Server (R5 FHIR)")
|
||||||
// .addServer()
|
.withSearchResultRowOperation(EXTOP_VALIDATE, id -> true)
|
||||||
// .withId("tdl_d2")
|
.withSearchResultRowOperation("$diff", id -> id.isVersionIdPartValidLong() && id.getVersionIdPartAsLong() > 1)
|
||||||
// .withFhirVersion(FhirVersionEnum.DSTU2)
|
.withSearchResultRowOperation("$everything", id -> "Patient".equals(id.getResourceType()))
|
||||||
// .withBaseUrl("http://hapi.fhir.org/testDataLibraryDstu2")
|
|
||||||
// .withName("Test Data Library (DSTU2 FHIR)")
|
// Non-HAPI servers follow
|
||||||
// .allowsApiKey()
|
|
||||||
// .addServer()
|
|
||||||
// .withId("tdl_d3")
|
|
||||||
// .withFhirVersion(FhirVersionEnum.DSTU3)
|
|
||||||
// .withBaseUrl("http://hapi.fhir.org/testDataLibraryStu3")
|
|
||||||
// .withName("Test Data Library (DSTU3 FHIR)")
|
|
||||||
// .allowsApiKey()
|
|
||||||
.addServer()
|
.addServer()
|
||||||
.withId("hi4")
|
.withId("hi4")
|
||||||
.withFhirVersion(FhirVersionEnum.DSTU3)
|
.withFhirVersion(FhirVersionEnum.DSTU3)
|
||||||
.withBaseUrl("http://test.fhir.org/r4")
|
.withBaseUrl("http://test.fhir.org/r4")
|
||||||
.withName("Health Intersections (R4 FHIR)")
|
.withName("Health Intersections (R4 FHIR)")
|
||||||
|
|
||||||
.addServer()
|
.addServer()
|
||||||
.withId("hi3")
|
.withId("hi3")
|
||||||
.withFhirVersion(FhirVersionEnum.DSTU3)
|
.withFhirVersion(FhirVersionEnum.DSTU3)
|
||||||
.withBaseUrl("http://test.fhir.org/r3")
|
.withBaseUrl("http://test.fhir.org/r3")
|
||||||
.withName("Health Intersections (STU3 FHIR)")
|
.withName("Health Intersections (STU3 FHIR)")
|
||||||
|
|
||||||
.addServer()
|
.addServer()
|
||||||
.withId("hi2")
|
.withId("hi2")
|
||||||
.withFhirVersion(FhirVersionEnum.DSTU2)
|
.withFhirVersion(FhirVersionEnum.DSTU2)
|
||||||
.withBaseUrl("http://test.fhir.org/r2")
|
.withBaseUrl("http://test.fhir.org/r2")
|
||||||
.withName("Health Intersections (DSTU2 FHIR)")
|
.withName("Health Intersections (DSTU2 FHIR)")
|
||||||
|
|
||||||
.addServer()
|
.addServer()
|
||||||
.withId("spark2")
|
.withId("spark2")
|
||||||
.withFhirVersion(FhirVersionEnum.DSTU3)
|
.withFhirVersion(FhirVersionEnum.DSTU3)
|
||||||
.withBaseUrl("http://vonk.fire.ly/")
|
.withBaseUrl("http://vonk.fire.ly/")
|
||||||
.withName("Vonk - Firely (STU3 FHIR)");
|
.withName("Vonk - Firely (STU3 FHIR)");
|
||||||
|
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(autowire=Autowire.BY_TYPE)
|
@Bean(autowire = Autowire.BY_TYPE)
|
||||||
public SubscriptionPlaygroundController subscriptionPlaygroundController() {
|
public SubscriptionPlaygroundController subscriptionPlaygroundController() {
|
||||||
return new SubscriptionPlaygroundController();
|
return new SubscriptionPlaygroundController();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
//@formatter:on
|
|
||||||
|
|
|
@ -191,7 +191,7 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
|
||||||
}
|
}
|
||||||
if (isBlank(nextResource.getIdElement().getVersionIdPart()) && nextResource instanceof IResource) {
|
if (isBlank(nextResource.getIdElement().getVersionIdPart()) && nextResource instanceof IResource) {
|
||||||
//TODO: Use of a deprecated method should be resolved.
|
//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()) {
|
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))");
|
throw new InternalErrorException(Msg.code(411) + "Server provided resource at index " + index + " with no Version ID set (using IResource#setId(IdDt))");
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,17 +20,16 @@ package ca.uhn.fhir.rest.server.provider;
|
||||||
* #L%
|
* #L%
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import ca.uhn.fhir.i18n.Msg;
|
|
||||||
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
|
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
|
||||||
import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
|
|
||||||
import ca.uhn.fhir.context.FhirContext;
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
import ca.uhn.fhir.context.FhirVersionEnum;
|
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.HookParams;
|
||||||
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
|
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
|
||||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
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.api.ResourceMetadataKeyEnum;
|
||||||
|
import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum;
|
||||||
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
|
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
|
||||||
import ca.uhn.fhir.rest.annotation.Create;
|
import ca.uhn.fhir.rest.annotation.Create;
|
||||||
import ca.uhn.fhir.rest.annotation.Delete;
|
import ca.uhn.fhir.rest.annotation.Delete;
|
||||||
|
@ -68,6 +67,7 @@ import org.slf4j.LoggerFactory;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -103,10 +103,10 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
||||||
protected LinkedList<T> myTypeHistory = new LinkedList<>();
|
protected LinkedList<T> myTypeHistory = new LinkedList<>();
|
||||||
protected AtomicLong mySearchCount = new AtomicLong(0);
|
protected AtomicLong mySearchCount = new AtomicLong(0);
|
||||||
private long myNextId;
|
private long myNextId;
|
||||||
private AtomicLong myDeleteCount = new AtomicLong(0);
|
private final AtomicLong myDeleteCount = new AtomicLong(0);
|
||||||
private AtomicLong myUpdateCount = new AtomicLong(0);
|
private final AtomicLong myUpdateCount = new AtomicLong(0);
|
||||||
private AtomicLong myCreateCount = new AtomicLong(0);
|
private final AtomicLong myCreateCount = new AtomicLong(0);
|
||||||
private AtomicLong myReadCount = new AtomicLong(0);
|
private final AtomicLong myReadCount = new AtomicLong(0);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
|
@ -134,7 +134,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
||||||
/**
|
/**
|
||||||
* Clear the counts used by {@link #getCountRead()} and other count methods
|
* Clear the counts used by {@link #getCountRead()} and other count methods
|
||||||
*/
|
*/
|
||||||
public synchronized void clearCounts() {
|
public synchronized void clearCounts() {
|
||||||
myReadCount.set(0L);
|
myReadCount.set(0L);
|
||||||
myUpdateCount.set(0L);
|
myUpdateCount.set(0L);
|
||||||
myCreateCount.set(0L);
|
myCreateCount.set(0L);
|
||||||
|
@ -163,12 +163,12 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
||||||
|
|
||||||
assert !myIdToVersionToResourceMap.containsKey(idPartAsString);
|
assert !myIdToVersionToResourceMap.containsKey(idPartAsString);
|
||||||
|
|
||||||
IIdType id = store(theResource, idPartAsString, versionIdPart, theRequestDetails, theTransactionDetails);
|
store(theResource, idPartAsString, versionIdPart, theRequestDetails, theTransactionDetails, false);
|
||||||
theResource.setId(id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({"unchecked"})
|
||||||
@Delete
|
@Delete
|
||||||
public synchronized MethodOutcome delete(@IdParam IIdType theId, RequestDetails theRequestDetails) {
|
public synchronized MethodOutcome delete(@IdParam IIdType theId, RequestDetails theRequestDetails) {
|
||||||
TransactionDetails transactionDetails = new TransactionDetails();
|
TransactionDetails transactionDetails = new TransactionDetails();
|
||||||
|
|
||||||
TreeMap<Long, T> versions = myIdToVersionToResourceMap.get(theId.getIdPart());
|
TreeMap<Long, T> versions = myIdToVersionToResourceMap.get(theId.getIdPart());
|
||||||
|
@ -176,9 +176,9 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
||||||
throw new ResourceNotFoundException(Msg.code(1979) + theId);
|
throw new ResourceNotFoundException(Msg.code(1979) + theId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
T deletedInstance = (T) myFhirContext.getResourceDefinition(myResourceType).newInstance();
|
||||||
long nextVersion = versions.lastEntry().getKey() + 1L;
|
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();
|
myDeleteCount.incrementAndGet();
|
||||||
|
|
||||||
|
@ -190,7 +190,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
||||||
* This method returns a simple operation count. This is mostly
|
* This method returns a simple operation count. This is mostly
|
||||||
* useful for testing purposes.
|
* useful for testing purposes.
|
||||||
*/
|
*/
|
||||||
public synchronized long getCountCreate() {
|
public synchronized long getCountCreate() {
|
||||||
return myCreateCount.get();
|
return myCreateCount.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,7 +198,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
||||||
* This method returns a simple operation count. This is mostly
|
* This method returns a simple operation count. This is mostly
|
||||||
* useful for testing purposes.
|
* useful for testing purposes.
|
||||||
*/
|
*/
|
||||||
public synchronized long getCountDelete() {
|
public synchronized long getCountDelete() {
|
||||||
return myDeleteCount.get();
|
return myDeleteCount.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,7 +206,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
||||||
* This method returns a simple operation count. This is mostly
|
* This method returns a simple operation count. This is mostly
|
||||||
* useful for testing purposes.
|
* useful for testing purposes.
|
||||||
*/
|
*/
|
||||||
public synchronized long getCountRead() {
|
public synchronized long getCountRead() {
|
||||||
return myReadCount.get();
|
return myReadCount.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,7 +214,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
||||||
* This method returns a simple operation count. This is mostly
|
* This method returns a simple operation count. This is mostly
|
||||||
* useful for testing purposes.
|
* useful for testing purposes.
|
||||||
*/
|
*/
|
||||||
public synchronized long getCountSearch() {
|
public synchronized long getCountSearch() {
|
||||||
return mySearchCount.get();
|
return mySearchCount.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,7 +222,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
||||||
* This method returns a simple operation count. This is mostly
|
* This method returns a simple operation count. This is mostly
|
||||||
* useful for testing purposes.
|
* useful for testing purposes.
|
||||||
*/
|
*/
|
||||||
public synchronized long getCountUpdate() {
|
public synchronized long getCountUpdate() {
|
||||||
return myUpdateCount.get();
|
return myUpdateCount.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,7 +237,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
||||||
}
|
}
|
||||||
|
|
||||||
@History
|
@History
|
||||||
public synchronized List<IBaseResource> historyInstance(@IdParam IIdType theId, RequestDetails theRequestDetails) {
|
public synchronized List<IBaseResource> historyInstance(@IdParam IIdType theId, RequestDetails theRequestDetails) {
|
||||||
LinkedList<T> retVal = myIdToHistory.get(theId.getIdPart());
|
LinkedList<T> retVal = myIdToHistory.get(theId.getIdPart());
|
||||||
if (retVal == null) {
|
if (retVal == null) {
|
||||||
throw new ResourceNotFoundException(Msg.code(1980) + theId);
|
throw new ResourceNotFoundException(Msg.code(1980) + theId);
|
||||||
|
@ -252,7 +252,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
||||||
}
|
}
|
||||||
|
|
||||||
@Read(version = true)
|
@Read(version = true)
|
||||||
public synchronized T read(@IdParam IIdType theId, RequestDetails theRequestDetails) {
|
public synchronized T read(@IdParam IIdType theId, RequestDetails theRequestDetails) {
|
||||||
TreeMap<Long, T> versions = myIdToVersionToResourceMap.get(theId.getIdPart());
|
TreeMap<Long, T> versions = myIdToVersionToResourceMap.get(theId.getIdPart());
|
||||||
if (versions == null || versions.isEmpty()) {
|
if (versions == null || versions.isEmpty()) {
|
||||||
throw new ResourceNotFoundException(Msg.code(1981) + theId);
|
throw new ResourceNotFoundException(Msg.code(1981) + theId);
|
||||||
|
@ -265,7 +265,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
||||||
throw new ResourceNotFoundException(Msg.code(1982) + theId);
|
throw new ResourceNotFoundException(Msg.code(1982) + theId);
|
||||||
} else {
|
} else {
|
||||||
T resource = versions.get(versionId);
|
T resource = versions.get(versionId);
|
||||||
if (resource == null) {
|
if (resource == null || ResourceMetadataKeyEnum.DELETED_AT.get(resource) != null) {
|
||||||
throw new ResourceGoneException(Msg.code(1983) + theId);
|
throw new ResourceGoneException(Msg.code(1983) + theId);
|
||||||
}
|
}
|
||||||
retVal = resource;
|
retVal = resource;
|
||||||
|
@ -285,21 +285,26 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
||||||
}
|
}
|
||||||
|
|
||||||
@Search
|
@Search
|
||||||
public synchronized List<IBaseResource> searchAll(RequestDetails theRequestDetails) {
|
public synchronized List<IBaseResource> searchAll(RequestDetails theRequestDetails) {
|
||||||
mySearchCount.incrementAndGet();
|
mySearchCount.incrementAndGet();
|
||||||
List<T> retVal = getAllResources();
|
List<T> retVal = getAllResources();
|
||||||
return fireInterceptorsAndFilterAsNeeded(retVal, theRequestDetails);
|
return fireInterceptorsAndFilterAsNeeded(retVal, theRequestDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
protected synchronized List<T> getAllResources() {
|
protected synchronized List<T> getAllResources() {
|
||||||
List<T> retVal = new ArrayList<>();
|
List<T> retVal = new ArrayList<>();
|
||||||
|
|
||||||
for (TreeMap<Long, T> next : myIdToVersionToResourceMap.values()) {
|
for (TreeMap<Long, T> next : myIdToVersionToResourceMap.values()) {
|
||||||
if (next.isEmpty() == false) {
|
if (next.isEmpty() == false) {
|
||||||
T nextResource = next.lastEntry().getValue();
|
T nextResource = next.lastEntry().getValue();
|
||||||
if (nextResource != null) {
|
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<T extends IBaseResource> implements IResour
|
||||||
}
|
}
|
||||||
|
|
||||||
@Search
|
@Search
|
||||||
public synchronized List<IBaseResource> searchById(
|
public synchronized List<IBaseResource> searchById(
|
||||||
@RequiredParam(name = "_id") TokenAndListParam theIds, RequestDetails theRequestDetails) {
|
@RequiredParam(name = "_id") TokenAndListParam theIds, RequestDetails theRequestDetails) {
|
||||||
|
|
||||||
List<T> retVal = new ArrayList<>();
|
List<T> retVal = new ArrayList<>();
|
||||||
|
@ -345,12 +350,25 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
||||||
return fireInterceptorsAndFilterAsNeeded(retVal, theRequestDetails);
|
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();
|
IIdType id = myFhirContext.getVersion().newIdType();
|
||||||
String versionIdPart = Long.toString(theVersionIdPart);
|
String versionIdPart = Long.toString(theVersionIdPart);
|
||||||
id.setParts(null, myResourceName, theIdPart, versionIdPart);
|
id.setParts(null, myResourceName, theIdPart, versionIdPart);
|
||||||
if (theResource != null) {
|
theResource.setId(id);
|
||||||
theResource.setId(id);
|
|
||||||
|
if (theDeleted) {
|
||||||
|
IPrimitiveType<Date> deletedAt = (IPrimitiveType<Date>) 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<T extends IBaseResource> implements IResour
|
||||||
* in the resource being stored accurately represents the version
|
* in the resource being stored accurately represents the version
|
||||||
* that was assigned by this provider
|
* that was assigned by this provider
|
||||||
*/
|
*/
|
||||||
if (theResource != null) {
|
if (myFhirContext.getVersion().getVersion() == FhirVersionEnum.DSTU2) {
|
||||||
if (myFhirContext.getVersion().getVersion() == FhirVersionEnum.DSTU2) {
|
ResourceMetadataKeyEnum.VERSION.put(theResource, versionIdPart);
|
||||||
ResourceMetadataKeyEnum.VERSION.put((IResource) theResource, versionIdPart);
|
} else {
|
||||||
} else {
|
BaseRuntimeChildDefinition metaChild = myFhirContext.getResourceDefinition(myResourceType).getChildByName("meta");
|
||||||
BaseRuntimeChildDefinition metaChild = myFhirContext.getResourceDefinition(myResourceType).getChildByName("meta");
|
List<IBase> metaValues = metaChild.getAccessor().getValues(theResource);
|
||||||
List<IBase> metaValues = metaChild.getAccessor().getValues(theResource);
|
if (metaValues.size() > 0) {
|
||||||
if (metaValues.size() > 0) {
|
theResource.getMeta().setVersionId(versionIdPart);
|
||||||
IBase meta = metaValues.get(0);
|
|
||||||
BaseRuntimeElementCompositeDefinition<?> metaDef = (BaseRuntimeElementCompositeDefinition<?>) myFhirContext.getElementDefinition(meta.getClass());
|
|
||||||
BaseRuntimeChildDefinition versionIdDef = metaDef.getChildByName("versionId");
|
|
||||||
List<IBase> versionIdValues = versionIdDef.getAccessor().getValues(meta);
|
|
||||||
if (versionIdValues.size() > 0) {
|
|
||||||
IPrimitiveType<?> versionId = (IPrimitiveType<?>) versionIdValues.get(0);
|
|
||||||
versionId.setValueAsString(versionIdPart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -383,52 +392,70 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
||||||
TreeMap<Long, T> versionToResource = getVersionToResource(theIdPart);
|
TreeMap<Long, T> versionToResource = getVersionToResource(theIdPart);
|
||||||
versionToResource.put(theVersionIdPart, theResource);
|
versionToResource.put(theVersionIdPart, theResource);
|
||||||
|
|
||||||
if (theRequestDetails != null) {
|
if (theRequestDetails != null && theRequestDetails.getInterceptorBroadcaster() != null) {
|
||||||
IInterceptorBroadcaster interceptorBroadcaster = theRequestDetails.getInterceptorBroadcaster();
|
IInterceptorBroadcaster interceptorBroadcaster = theRequestDetails.getInterceptorBroadcaster();
|
||||||
|
|
||||||
if (theResource != null) {
|
if (theDeleted) {
|
||||||
if (!myIdToHistory.containsKey(theIdPart)) {
|
|
||||||
|
|
||||||
// Interceptor call: STORAGE_PRESTORAGE_RESOURCE_CREATED
|
// Interceptor call: STORAGE_PRESTORAGE_RESOURCE_DELETED
|
||||||
HookParams preStorageParams = new HookParams()
|
HookParams preStorageParams = new HookParams()
|
||||||
.add(RequestDetails.class, theRequestDetails)
|
.add(RequestDetails.class, theRequestDetails)
|
||||||
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
|
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
|
||||||
.add(IBaseResource.class, theResource)
|
.add(IBaseResource.class, myIdToHistory.get(theIdPart).getFirst())
|
||||||
.add(RequestPartitionId.class, null) // we should add this if we want - but this is test usage
|
.add(TransactionDetails.class, theTransactionDetails);
|
||||||
.add(TransactionDetails.class, theTransactionDetails);
|
interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, preStorageParams);
|
||||||
interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, preStorageParams);
|
|
||||||
|
|
||||||
// Interceptor call: STORAGE_PRECOMMIT_RESOURCE_CREATED
|
// Interceptor call: STORAGE_PRECOMMIT_RESOURCE_DELETED
|
||||||
HookParams preCommitParams = new HookParams()
|
HookParams preCommitParams = new HookParams()
|
||||||
.add(RequestDetails.class, theRequestDetails)
|
.add(RequestDetails.class, theRequestDetails)
|
||||||
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
|
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
|
||||||
.add(IBaseResource.class, theResource)
|
.add(IBaseResource.class, myIdToHistory.get(theIdPart).getFirst())
|
||||||
.add(TransactionDetails.class, theTransactionDetails)
|
.add(TransactionDetails.class, theTransactionDetails)
|
||||||
.add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED));
|
.add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED));
|
||||||
interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED, preCommitParams);
|
interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, preCommitParams);
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// Interceptor call: STORAGE_PRESTORAGE_RESOURCE_UPDATED
|
} else if (!myIdToHistory.containsKey(theIdPart)) {
|
||||||
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
|
// Interceptor call: STORAGE_PRESTORAGE_RESOURCE_CREATED
|
||||||
HookParams preCommitParams = new HookParams()
|
HookParams preStorageParams = new HookParams()
|
||||||
.add(RequestDetails.class, theRequestDetails)
|
.add(RequestDetails.class, theRequestDetails)
|
||||||
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
|
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
|
||||||
.add(IBaseResource.class, myIdToHistory.get(theIdPart).getFirst())
|
.add(IBaseResource.class, theResource)
|
||||||
.add(IBaseResource.class, theResource)
|
.add(RequestPartitionId.class, null) // we should add this if we want - but this is test usage
|
||||||
.add(TransactionDetails.class, theTransactionDetails)
|
.add(TransactionDetails.class, theTransactionDetails);
|
||||||
.add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED));
|
interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, preStorageParams);
|
||||||
interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, preCommitParams);
|
|
||||||
|
// 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<T extends IBaseResource> implements IResour
|
||||||
* @param theConditional This is provided only so that subclasses can implement if they want
|
* @param theConditional This is provided only so that subclasses can implement if they want
|
||||||
*/
|
*/
|
||||||
@Update
|
@Update
|
||||||
public synchronized MethodOutcome update(
|
public synchronized MethodOutcome update(
|
||||||
@ResourceParam T theResource,
|
@ResourceParam T theResource,
|
||||||
@ConditionalUrlParam String theConditional,
|
@ConditionalUrlParam String theConditional,
|
||||||
RequestDetails theRequestDetails) {
|
RequestDetails theRequestDetails) {
|
||||||
|
@ -478,7 +505,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
||||||
created = false;
|
created = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
IIdType id = store(theResource, idPartAsString, versionIdPart, theRequestDetails, theTransactionDetails);
|
IIdType id = store(theResource, idPartAsString, versionIdPart, theRequestDetails, theTransactionDetails, false);
|
||||||
theResource.setId(id);
|
theResource.setId(id);
|
||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
|
@ -495,7 +522,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
||||||
* @param theResource The resource to store. If the resource has an ID, that ID is updated.
|
* @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
|
* @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()) {
|
if (theResource.getIdElement().hasIdPart()) {
|
||||||
updateInternal(theResource, null, new TransactionDetails());
|
updateInternal(theResource, null, new TransactionDetails());
|
||||||
} else {
|
} else {
|
||||||
|
@ -509,7 +536,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
||||||
*
|
*
|
||||||
* @since 4.1.0
|
* @since 4.1.0
|
||||||
*/
|
*/
|
||||||
public synchronized List<T> getStoredResources() {
|
public synchronized List<T> getStoredResources() {
|
||||||
List<T> retVal = new ArrayList<>();
|
List<T> retVal = new ArrayList<>();
|
||||||
for (TreeMap<Long, T> next : myIdToVersionToResourceMap.values()) {
|
for (TreeMap<Long, T> next : myIdToVersionToResourceMap.values()) {
|
||||||
retVal.add(next.lastEntry().getValue());
|
retVal.add(next.lastEntry().getValue());
|
||||||
|
|
|
@ -103,7 +103,7 @@ public class DefaultThymeleafNarrativeGeneratorDstu2Test {
|
||||||
|
|
||||||
ourLog.info(output);
|
ourLog.info(output);
|
||||||
|
|
||||||
assertThat(output, containsString("<td><pre>YThis is a warning</pre></td>"));
|
assertThat(output, containsString("<td>YThis is a warning</td>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -163,7 +163,7 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test {
|
||||||
|
|
||||||
ourLog.info(output);
|
ourLog.info(output);
|
||||||
|
|
||||||
assertThat(output, containsString("<td><pre>YThis is a warning</pre></td>"));
|
assertThat(output, containsString("<td>YThis is a warning</td>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -111,7 +111,7 @@ public class DefaultThymeleafNarrativeGeneratorR4Test {
|
||||||
|
|
||||||
ourLog.info(output);
|
ourLog.info(output);
|
||||||
|
|
||||||
assertThat(output, containsString("<td><pre>YThis is a warning</pre></td>"));
|
assertThat(output, containsString("<td>YThis is a warning</td>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -3,35 +3,31 @@ package ca.uhn.fhir.rest.server.provider;
|
||||||
import ca.uhn.fhir.context.FhirContext;
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor;
|
import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor;
|
||||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
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.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.ResourceGoneException;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
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 ca.uhn.fhir.util.TestUtil;
|
||||||
import org.eclipse.jetty.server.Server;
|
import ca.uhn.fhir.validation.FhirValidator;
|
||||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
import ca.uhn.fhir.validation.ResultSeverityEnum;
|
||||||
import org.eclipse.jetty.servlet.ServletHolder;
|
|
||||||
import org.hl7.fhir.instance.model.api.IAnyResource;
|
import org.hl7.fhir.instance.model.api.IAnyResource;
|
||||||
import org.hl7.fhir.instance.model.api.IIdType;
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
import org.hl7.fhir.r4.model.Bundle;
|
import org.hl7.fhir.r4.model.Bundle;
|
||||||
import org.hl7.fhir.r4.model.Observation;
|
import org.hl7.fhir.r4.model.Observation;
|
||||||
import org.hl7.fhir.r4.model.Patient;
|
import org.hl7.fhir.r4.model.Patient;
|
||||||
import org.junit.jupiter.api.AfterAll;
|
import org.junit.jupiter.api.AfterAll;
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
import org.junit.jupiter.api.Order;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.servlet.ServletException;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
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.containsInAnyOrder;
|
||||||
import static org.hamcrest.Matchers.matchesPattern;
|
import static org.hamcrest.Matchers.matchesPattern;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
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.junit.jupiter.api.Assertions.fail;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
@ -48,24 +46,22 @@ import static org.mockito.Mockito.verify;
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
public class HashMapResourceProviderTest {
|
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<Patient> myPatientResourceProvider = new HashMapResourceProviderExtension<>(ourRestServer, Patient.class);
|
||||||
|
@RegisterExtension
|
||||||
|
@Order(2)
|
||||||
|
private static final HashMapResourceProviderExtension<Observation> myObservationResourceProvider = new HashMapResourceProviderExtension<>(ourRestServer, Observation.class);
|
||||||
|
|
||||||
private static final Logger ourLog = LoggerFactory.getLogger(HashMapResourceProviderTest.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<Patient> myPatientResourceProvider;
|
|
||||||
private static HashMapResourceProvider<Observation> myObservationResourceProvider;
|
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private IAnonymousInterceptor myAnonymousInterceptor;
|
private IAnonymousInterceptor myAnonymousInterceptor;
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
public void before() {
|
|
||||||
ourRestServer.clearData();
|
|
||||||
myPatientResourceProvider.clearCounts();
|
|
||||||
myObservationResourceProvider.clearCounts();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCreateAndRead() {
|
public void testCreateAndRead() {
|
||||||
ourRestServer.getInterceptorService().registerAnonymousInterceptor(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, myAnonymousInterceptor);
|
ourRestServer.getInterceptorService().registerAnonymousInterceptor(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, myAnonymousInterceptor);
|
||||||
|
@ -74,7 +70,7 @@ public class HashMapResourceProviderTest {
|
||||||
// Create
|
// Create
|
||||||
Patient p = new Patient();
|
Patient p = new Patient();
|
||||||
p.setActive(true);
|
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]+"));
|
assertThat(id.getIdPart(), matchesPattern("[0-9]+"));
|
||||||
assertEquals("1", id.getVersionIdPart());
|
assertEquals("1", id.getVersionIdPart());
|
||||||
|
|
||||||
|
@ -82,8 +78,8 @@ public class HashMapResourceProviderTest {
|
||||||
verify(myAnonymousInterceptor, Mockito.times(1)).invoke(eq(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED), any());
|
verify(myAnonymousInterceptor, Mockito.times(1)).invoke(eq(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED), any());
|
||||||
|
|
||||||
// Read
|
// Read
|
||||||
p = (Patient) ourClient.read().resource("Patient").withId(id).execute();
|
p = (Patient) ourRestServer.getFhirClient().read().resource("Patient").withId(id).execute();
|
||||||
assertEquals(true, p.getActive());
|
assertTrue(p.getActive());
|
||||||
|
|
||||||
assertEquals(1, myPatientResourceProvider.getCountRead());
|
assertEquals(1, myPatientResourceProvider.getCountRead());
|
||||||
}
|
}
|
||||||
|
@ -94,13 +90,13 @@ public class HashMapResourceProviderTest {
|
||||||
Patient p = new Patient();
|
Patient p = new Patient();
|
||||||
p.setId("ABC");
|
p.setId("ABC");
|
||||||
p.setActive(true);
|
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("ABC", id.getIdPart());
|
||||||
assertEquals("1", id.getVersionIdPart());
|
assertEquals("1", id.getVersionIdPart());
|
||||||
|
|
||||||
// Read
|
// Read
|
||||||
p = (Patient) ourClient.read().resource("Patient").withId(id).execute();
|
p = (Patient) ourRestServer.getFhirClient().read().resource("Patient").withId(id).execute();
|
||||||
assertEquals(true, p.getActive());
|
assertTrue(p.getActive());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -108,31 +104,39 @@ public class HashMapResourceProviderTest {
|
||||||
// Create
|
// Create
|
||||||
Patient p = new Patient();
|
Patient p = new Patient();
|
||||||
p.setActive(true);
|
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]+"));
|
assertThat(id.getIdPart(), matchesPattern("[0-9]+"));
|
||||||
assertEquals("1", id.getVersionIdPart());
|
assertEquals("1", id.getVersionIdPart());
|
||||||
|
|
||||||
assertEquals(0, myPatientResourceProvider.getCountDelete());
|
assertEquals(0, myPatientResourceProvider.getCountDelete());
|
||||||
|
|
||||||
IDeleteTyped iDeleteTyped = ourClient.delete().resourceById(id.toUnqualifiedVersionless());
|
ourRestServer.getFhirClient().delete().resourceById(id.toUnqualifiedVersionless()).execute();
|
||||||
ourLog.info("About to execute");
|
ourLog.info("About to execute");
|
||||||
try {
|
|
||||||
iDeleteTyped.execute();
|
|
||||||
} catch (NullPointerException e) {
|
|
||||||
ourLog.error("NPE", e);
|
|
||||||
fail(e.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
assertEquals(1, myPatientResourceProvider.getCountDelete());
|
assertEquals(1, myPatientResourceProvider.getCountDelete());
|
||||||
|
|
||||||
// Read
|
// Read
|
||||||
ourClient.read().resource("Patient").withId(id.withVersion("1")).execute();
|
ourRestServer.getFhirClient().read().resource("Patient").withId(id.withVersion("1")).execute();
|
||||||
try {
|
try {
|
||||||
ourClient.read().resource("Patient").withId(id.withVersion("2")).execute();
|
ourRestServer.getFhirClient().read().resource("Patient").withId(id.withVersion("2")).execute();
|
||||||
fail();
|
fail();
|
||||||
} catch (ResourceGoneException e) {
|
} catch (ResourceGoneException e) {
|
||||||
// good
|
// 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
|
@Test
|
||||||
|
@ -140,14 +144,14 @@ public class HashMapResourceProviderTest {
|
||||||
// Create Res 1
|
// Create Res 1
|
||||||
Patient p = new Patient();
|
Patient p = new Patient();
|
||||||
p.setActive(true);
|
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]+"));
|
assertThat(id1.getIdPart(), matchesPattern("[0-9]+"));
|
||||||
assertEquals("1", id1.getVersionIdPart());
|
assertEquals("1", id1.getVersionIdPart());
|
||||||
|
|
||||||
// Create Res 2
|
// Create Res 2
|
||||||
p = new Patient();
|
p = new Patient();
|
||||||
p.setActive(true);
|
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]+"));
|
assertThat(id2.getIdPart(), matchesPattern("[0-9]+"));
|
||||||
assertEquals("1", id2.getVersionIdPart());
|
assertEquals("1", id2.getVersionIdPart());
|
||||||
|
|
||||||
|
@ -155,11 +159,11 @@ public class HashMapResourceProviderTest {
|
||||||
p = new Patient();
|
p = new Patient();
|
||||||
p.setId(id2);
|
p.setId(id2);
|
||||||
p.setActive(false);
|
p.setActive(false);
|
||||||
id2 = ourClient.update().resource(p).execute().getId();
|
id2 = ourRestServer.getFhirClient().update().resource(p).execute().getId();
|
||||||
assertThat(id2.getIdPart(), matchesPattern("[0-9]+"));
|
assertThat(id2.getIdPart(), matchesPattern("[0-9]+"));
|
||||||
assertEquals("2", id2.getVersionIdPart());
|
assertEquals("2", id2.getVersionIdPart());
|
||||||
|
|
||||||
Bundle history = ourClient
|
Bundle history = ourRestServer.getFhirClient()
|
||||||
.history()
|
.history()
|
||||||
.onInstance(id2.toUnqualifiedVersionless())
|
.onInstance(id2.toUnqualifiedVersionless())
|
||||||
.andReturnBundle(Bundle.class)
|
.andReturnBundle(Bundle.class)
|
||||||
|
@ -184,14 +188,14 @@ public class HashMapResourceProviderTest {
|
||||||
// Create Res 1
|
// Create Res 1
|
||||||
Patient p = new Patient();
|
Patient p = new Patient();
|
||||||
p.setActive(true);
|
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]+"));
|
assertThat(id1.getIdPart(), matchesPattern("[0-9]+"));
|
||||||
assertEquals("1", id1.getVersionIdPart());
|
assertEquals("1", id1.getVersionIdPart());
|
||||||
|
|
||||||
// Create Res 2
|
// Create Res 2
|
||||||
p = new Patient();
|
p = new Patient();
|
||||||
p.setActive(true);
|
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]+"));
|
assertThat(id2.getIdPart(), matchesPattern("[0-9]+"));
|
||||||
assertEquals("1", id2.getVersionIdPart());
|
assertEquals("1", id2.getVersionIdPart());
|
||||||
|
|
||||||
|
@ -199,11 +203,11 @@ public class HashMapResourceProviderTest {
|
||||||
p = new Patient();
|
p = new Patient();
|
||||||
p.setId(id2);
|
p.setId(id2);
|
||||||
p.setActive(false);
|
p.setActive(false);
|
||||||
id2 = ourClient.update().resource(p).execute().getId();
|
id2 = ourRestServer.getFhirClient().update().resource(p).execute().getId();
|
||||||
assertThat(id2.getIdPart(), matchesPattern("[0-9]+"));
|
assertThat(id2.getIdPart(), matchesPattern("[0-9]+"));
|
||||||
assertEquals("2", id2.getVersionIdPart());
|
assertEquals("2", id2.getVersionIdPart());
|
||||||
|
|
||||||
Bundle history = ourClient
|
Bundle history = ourRestServer.getFhirClient()
|
||||||
.history()
|
.history()
|
||||||
.onType(Patient.class)
|
.onType(Patient.class)
|
||||||
.andReturnBundle(Bundle.class)
|
.andReturnBundle(Bundle.class)
|
||||||
|
@ -228,20 +232,23 @@ public class HashMapResourceProviderTest {
|
||||||
for (int i = 0; i < 100; i++) {
|
for (int i = 0; i < 100; i++) {
|
||||||
Patient p = new Patient();
|
Patient p = new Patient();
|
||||||
p.addName().setFamily("FAM" + i);
|
p.addName().setFamily("FAM" + i);
|
||||||
ourClient.registerInterceptor(new LoggingInterceptor(true));
|
ourRestServer.getFhirClient().registerInterceptor(new LoggingInterceptor(true));
|
||||||
IIdType id = ourClient.create().resource(p).execute().getId();
|
IIdType id = ourRestServer.getFhirClient().create().resource(p).execute().getId();
|
||||||
assertThat(id.getIdPart(), matchesPattern("[0-9]+"));
|
assertThat(id.getIdPart(), matchesPattern("[0-9]+"));
|
||||||
assertEquals("1", id.getVersionIdPart());
|
assertEquals("1", id.getVersionIdPart());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
Bundle resp = ourClient
|
Bundle resp = ourRestServer.getFhirClient()
|
||||||
.search()
|
.search()
|
||||||
.forResource("Patient")
|
.forResource("Patient")
|
||||||
.returnBundle(Bundle.class)
|
.returnBundle(Bundle.class)
|
||||||
.execute();
|
.execute();
|
||||||
|
ourLog.info("Search:\n{}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(resp));
|
||||||
assertEquals(100, resp.getTotal());
|
assertEquals(100, resp.getTotal());
|
||||||
assertEquals(100, resp.getEntry().size());
|
assertEquals(100, resp.getEntry().size());
|
||||||
|
assertFalse(resp.getEntry().get(0).hasRequest());
|
||||||
|
assertFalse(resp.getEntry().get(1).hasRequest());
|
||||||
|
|
||||||
assertEquals(1, myPatientResourceProvider.getCountSearch());
|
assertEquals(1, myPatientResourceProvider.getCountSearch());
|
||||||
|
|
||||||
|
@ -253,13 +260,13 @@ public class HashMapResourceProviderTest {
|
||||||
for (int i = 0; i < 100; i++) {
|
for (int i = 0; i < 100; i++) {
|
||||||
Patient p = new Patient();
|
Patient p = new Patient();
|
||||||
p.addName().setFamily("FAM" + i);
|
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]+"));
|
assertThat(id.getIdPart(), matchesPattern("[0-9]+"));
|
||||||
assertEquals("1", id.getVersionIdPart());
|
assertEquals("1", id.getVersionIdPart());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
Bundle resp = ourClient
|
Bundle resp = ourRestServer.getFhirClient()
|
||||||
.search()
|
.search()
|
||||||
.forResource("Patient")
|
.forResource("Patient")
|
||||||
.where(IAnyResource.RES_ID.exactly().codes("2", "3"))
|
.where(IAnyResource.RES_ID.exactly().codes("2", "3"))
|
||||||
|
@ -270,7 +277,7 @@ public class HashMapResourceProviderTest {
|
||||||
assertThat(respIds, containsInAnyOrder("Patient/2", "Patient/3"));
|
assertThat(respIds, containsInAnyOrder("Patient/2", "Patient/3"));
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
resp = ourClient
|
resp = ourRestServer.getFhirClient()
|
||||||
.search()
|
.search()
|
||||||
.forResource("Patient")
|
.forResource("Patient")
|
||||||
.where(IAnyResource.RES_ID.exactly().codes("2", "3"))
|
.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());
|
respIds = resp.getEntry().stream().map(t -> t.getResource().getIdElement().toUnqualifiedVersionless().getValue()).collect(Collectors.toList());
|
||||||
assertThat(respIds, containsInAnyOrder("Patient/2", "Patient/3"));
|
assertThat(respIds, containsInAnyOrder("Patient/2", "Patient/3"));
|
||||||
|
|
||||||
resp = ourClient
|
resp = ourRestServer.getFhirClient()
|
||||||
.search()
|
.search()
|
||||||
.forResource("Patient")
|
.forResource("Patient")
|
||||||
.where(IAnyResource.RES_ID.exactly().codes("2", "3"))
|
.where(IAnyResource.RES_ID.exactly().codes("2", "3"))
|
||||||
|
@ -299,7 +306,7 @@ public class HashMapResourceProviderTest {
|
||||||
// Create
|
// Create
|
||||||
Patient p = new Patient();
|
Patient p = new Patient();
|
||||||
p.setActive(true);
|
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]+"));
|
assertThat(id.getIdPart(), matchesPattern("[0-9]+"));
|
||||||
assertEquals("1", id.getVersionIdPart());
|
assertEquals("1", id.getVersionIdPart());
|
||||||
|
|
||||||
|
@ -310,7 +317,7 @@ public class HashMapResourceProviderTest {
|
||||||
p = new Patient();
|
p = new Patient();
|
||||||
p.setId(id);
|
p.setId(id);
|
||||||
p.setActive(false);
|
p.setActive(false);
|
||||||
id = ourClient.update().resource(p).execute().getId();
|
id = ourRestServer.getFhirClient().update().resource(p).execute().getId();
|
||||||
assertThat(id.getIdPart(), matchesPattern("[0-9]+"));
|
assertThat(id.getIdPart(), matchesPattern("[0-9]+"));
|
||||||
assertEquals("2", id.getVersionIdPart());
|
assertEquals("2", id.getVersionIdPart());
|
||||||
|
|
||||||
|
@ -321,71 +328,21 @@ public class HashMapResourceProviderTest {
|
||||||
assertEquals(1, myPatientResourceProvider.getCountUpdate());
|
assertEquals(1, myPatientResourceProvider.getCountUpdate());
|
||||||
|
|
||||||
// Read
|
// Read
|
||||||
p = (Patient) ourClient.read().resource("Patient").withId(id.withVersion("1")).execute();
|
p = (Patient) ourRestServer.getFhirClient().read().resource("Patient").withId(id.withVersion("1")).execute();
|
||||||
assertEquals(true, p.getActive());
|
assertTrue(p.getActive());
|
||||||
p = (Patient) ourClient.read().resource("Patient").withId(id.withVersion("2")).execute();
|
p = (Patient) ourRestServer.getFhirClient().read().resource("Patient").withId(id.withVersion("2")).execute();
|
||||||
assertEquals(false, p.getActive());
|
assertFalse(p.getActive());
|
||||||
try {
|
try {
|
||||||
ourClient.read().resource("Patient").withId(id.withVersion("3")).execute();
|
ourRestServer.getFhirClient().read().resource("Patient").withId(id.withVersion("3")).execute();
|
||||||
fail();
|
fail();
|
||||||
} catch (ResourceNotFoundException e) {
|
} catch (ResourceNotFoundException e) {
|
||||||
// good
|
// 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
|
@AfterAll
|
||||||
public static void afterClassClearContext() throws Exception {
|
public static void afterClassClearContext() throws Exception {
|
||||||
JettyUtil.closeServer(ourListenerServer);
|
|
||||||
TestUtil.randomizeLocaleAndTimezone();
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,6 +76,7 @@ public class HashMapResourceProviderExtension<T extends IBaseResource> extends H
|
||||||
myRestfulServerExtension.getRestfulServer().registerProvider(HashMapResourceProviderExtension.this);
|
myRestfulServerExtension.getRestfulServer().registerProvider(HashMapResourceProviderExtension.this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public synchronized MethodOutcome update(T theResource, String theConditional, RequestDetails theRequestDetails) {
|
public synchronized MethodOutcome update(T theResource, String theConditional, RequestDetails theRequestDetails) {
|
||||||
T resourceClone = getFhirContext().newTerser().clone(theResource);
|
T resourceClone = getFhirContext().newTerser().clone(theResource);
|
||||||
myUpdates.add(resourceClone);
|
myUpdates.add(resourceClone);
|
||||||
|
|
|
@ -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.client.api.ServerValidationModeEnum;
|
||||||
import ca.uhn.fhir.rest.server.IPagingProvider;
|
import ca.uhn.fhir.rest.server.IPagingProvider;
|
||||||
import ca.uhn.fhir.rest.server.RestfulServer;
|
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.Validate;
|
||||||
import org.apache.commons.lang3.time.DateUtils;
|
import org.apache.commons.lang3.time.DateUtils;
|
||||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<parent>
|
<parent>
|
||||||
|
@ -98,19 +99,6 @@
|
||||||
<artifactId>jakarta.annotation-api</artifactId>
|
<artifactId>jakarta.annotation-api</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>ch.qos.logback</groupId>
|
|
||||||
<artifactId>logback-classic</artifactId>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Test Database -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.derby</groupId>
|
|
||||||
<artifactId>derby</artifactId>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Spring -->
|
<!-- Spring -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework</groupId>
|
<groupId>org.springframework</groupId>
|
||||||
|
@ -189,7 +177,12 @@
|
||||||
<artifactId>popper.js</artifactId>
|
<artifactId>popper.js</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Test Dependencies -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>ch.qos.logback</groupId>
|
||||||
|
<artifactId>logback-classic</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.eclipse.jetty</groupId>
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
<artifactId>jetty-servlets</artifactId>
|
<artifactId>jetty-servlets</artifactId>
|
||||||
|
@ -220,6 +213,22 @@
|
||||||
<artifactId>commons-dbcp2</artifactId>
|
<artifactId>commons-dbcp2</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.sourceforge.htmlunit</groupId>
|
||||||
|
<artifactId>htmlunit</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||||
|
<artifactId>hapi-fhir-test-utilities</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
@ -235,9 +244,11 @@
|
||||||
</goals>
|
</goals>
|
||||||
<configuration>
|
<configuration>
|
||||||
<target>
|
<target>
|
||||||
<copy todir="${project.build.directory}/${project.build.finalName}/css" flatten="true" failonerror="true">
|
<copy todir="${project.build.directory}/${project.build.finalName}/css" flatten="true"
|
||||||
|
failonerror="true">
|
||||||
<resources>
|
<resources>
|
||||||
<file file="${basedir}/../hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/hapi-narrative.css" />
|
<file
|
||||||
|
file="${basedir}/../hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/hapi-narrative.css"/>
|
||||||
</resources>
|
</resources>
|
||||||
</copy>
|
</copy>
|
||||||
</target>
|
</target>
|
||||||
|
@ -262,17 +273,17 @@
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-source-plugin</artifactId>
|
<artifactId>maven-source-plugin</artifactId>
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
<phase>package</phase>
|
<phase>package</phase>
|
||||||
<goals>
|
<goals>
|
||||||
<goal>jar-no-fork</goal>
|
<goal>jar-no-fork</goal>
|
||||||
</goals>
|
</goals>
|
||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
|
|
@ -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.api.IHttpResponse;
|
||||||
import ca.uhn.fhir.rest.client.impl.GenericClient;
|
import ca.uhn.fhir.rest.client.impl.GenericClient;
|
||||||
import ca.uhn.fhir.to.model.HomeRequest;
|
import ca.uhn.fhir.to.model.HomeRequest;
|
||||||
|
import ca.uhn.fhir.util.BundleUtil;
|
||||||
import ca.uhn.fhir.util.ExtensionConstants;
|
import ca.uhn.fhir.util.ExtensionConstants;
|
||||||
import ca.uhn.hapi.converters.canonical.VersionCanonicalizer;
|
import ca.uhn.hapi.converters.canonical.VersionCanonicalizer;
|
||||||
import org.apache.commons.io.IOUtils;
|
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.entity.ContentType;
|
||||||
import org.apache.http.message.BasicHeader;
|
import org.apache.http.message.BasicHeader;
|
||||||
import org.hl7.fhir.instance.model.api.IAnyResource;
|
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.IBaseConformance;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
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.instance.model.api.IDomainResource;
|
||||||
import org.hl7.fhir.r5.model.CapabilityStatement;
|
import org.hl7.fhir.r5.model.CapabilityStatement;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
@ -373,23 +376,38 @@ public class BaseController {
|
||||||
|
|
||||||
private String parseNarrative(HomeRequest theRequest, EncodingEnum theCtEnum, String theResultBody) {
|
private String parseNarrative(HomeRequest theRequest, EncodingEnum theCtEnum, String theResultBody) {
|
||||||
try {
|
try {
|
||||||
IBaseResource par = theCtEnum.newParser(getContext(theRequest)).parseResource(theResultBody);
|
FhirContext context = getContext(theRequest);
|
||||||
String retVal;
|
IBaseResource result = theCtEnum.newParser(context).parseResource(theResultBody);
|
||||||
if (par instanceof IResource) {
|
return parseNarrative(context, result);
|
||||||
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);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
ourLog.error("Failed to parse resource", e);
|
ourLog.error("Failed to parse resource", e);
|
||||||
return "";
|
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) {
|
protected String preProcessMessageBody(String theBody) {
|
||||||
if (theBody == null) {
|
if (theBody == null) {
|
||||||
return "";
|
return "";
|
||||||
|
@ -462,7 +480,6 @@ public class BaseController {
|
||||||
switch (ctEnum) {
|
switch (ctEnum) {
|
||||||
case JSON:
|
case JSON:
|
||||||
if (theResultType == ResultType.RESOURCE) {
|
if (theResultType == ResultType.RESOURCE) {
|
||||||
narrativeString = parseNarrative(theRequest, ctEnum, resultBody);
|
|
||||||
resultDescription.append("JSON resource");
|
resultDescription.append("JSON resource");
|
||||||
} else if (theResultType == ResultType.BUNDLE) {
|
} else if (theResultType == ResultType.BUNDLE) {
|
||||||
resultDescription.append("JSON bundle");
|
resultDescription.append("JSON bundle");
|
||||||
|
@ -472,7 +489,6 @@ public class BaseController {
|
||||||
case XML:
|
case XML:
|
||||||
default:
|
default:
|
||||||
if (theResultType == ResultType.RESOURCE) {
|
if (theResultType == ResultType.RESOURCE) {
|
||||||
narrativeString = parseNarrative(theRequest, ctEnum, resultBody);
|
|
||||||
resultDescription.append("XML resource");
|
resultDescription.append("XML resource");
|
||||||
} else if (theResultType == ResultType.BUNDLE) {
|
} else if (theResultType == ResultType.BUNDLE) {
|
||||||
resultDescription.append("XML bundle");
|
resultDescription.append("XML bundle");
|
||||||
|
@ -480,6 +496,7 @@ public class BaseController {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
narrativeString = parseNarrative(theRequest, ctEnum, resultBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
resultDescription.append(" (").append(defaultString(resultBody).length() + " bytes)");
|
resultDescription.append(" (").append(defaultString(resultBody).length() + " bytes)");
|
||||||
|
@ -508,6 +525,9 @@ public class BaseController {
|
||||||
theModelMap.put("narrative", narrativeString);
|
theModelMap.put("narrative", narrativeString);
|
||||||
theModelMap.put("latencyMs", theLatency);
|
theModelMap.put("latencyMs", theLatency);
|
||||||
|
|
||||||
|
theModelMap.put("config", myConfig);
|
||||||
|
theModelMap.put("serverId", theRequest.getServerId());
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
ourLog.error("Failure during processing", e);
|
ourLog.error("Failure during processing", e);
|
||||||
theModelMap.put("errorMsg", toDisplayError("Error during processing: " + e.getMessage(), e));
|
theModelMap.put("errorMsg", toDisplayError("Error during processing: " + e.getMessage(), e));
|
||||||
|
|
|
@ -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.QuantityClientParam.IAndUnits;
|
||||||
import ca.uhn.fhir.rest.gclient.StringClientParam;
|
import ca.uhn.fhir.rest.gclient.StringClientParam;
|
||||||
import ca.uhn.fhir.rest.gclient.TokenClientParam;
|
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.HomeRequest;
|
||||||
import ca.uhn.fhir.to.model.ResourceRequest;
|
import ca.uhn.fhir.to.model.ResourceRequest;
|
||||||
import ca.uhn.fhir.to.model.TransactionRequest;
|
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 com.google.gson.stream.JsonWriter;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.hl7.fhir.dstu3.model.CapabilityStatement;
|
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.dstu3.model.StringType;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseConformance;
|
import org.hl7.fhir.instance.model.api.IBaseConformance;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseParameters;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
import org.springframework.ui.ModelMap;
|
import org.springframework.ui.ModelMap;
|
||||||
import org.springframework.validation.BindingResult;
|
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<? extends IBaseResource> type = getContext(theRequest).getResourceDefinition(instanceType).getImplementingClass();
|
||||||
|
Class<? extends IBaseParameters> parametersType = (Class<? extends IBaseParameters>) 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) {
|
private void doActionHistory(HttpServletRequest theReq, HomeRequest theRequest, BindingResult theBindingResult, ModelMap theModel, String theMethod, String theMethodDescription) {
|
||||||
addCommonParams(theReq, theRequest, theModel);
|
addCommonParams(theReq, theRequest, theModel);
|
||||||
|
|
||||||
|
|
|
@ -8,20 +8,22 @@ import org.springframework.context.annotation.ComponentScan;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
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.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
|
||||||
import org.thymeleaf.spring5.SpringTemplateEngine;
|
import org.thymeleaf.spring5.SpringTemplateEngine;
|
||||||
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
|
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
|
||||||
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
|
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
|
||||||
import org.thymeleaf.templatemode.TemplateMode;
|
import org.thymeleaf.templatemode.TemplateMode;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebMvc
|
@EnableWebMvc
|
||||||
@ComponentScan(basePackages = "ca.uhn.fhir.to")
|
@ComponentScan(basePackages = "ca.uhn.fhir.to")
|
||||||
public class FhirTesterMvcConfig extends WebMvcConfigurerAdapter {
|
public class FhirTesterMvcConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addResourceHandlers(ResourceHandlerRegistry theRegistry) {
|
public void addResourceHandlers(@Nonnull ResourceHandlerRegistry theRegistry) {
|
||||||
WebUtil.webJarAddBoostrap(theRegistry);
|
WebUtil.webJarAddBoostrap(theRegistry);
|
||||||
WebUtil.webJarAddJQuery(theRegistry);
|
WebUtil.webJarAddJQuery(theRegistry);
|
||||||
WebUtil.webJarAddFontAwesome(theRegistry);
|
WebUtil.webJarAddFontAwesome(theRegistry);
|
||||||
|
@ -40,13 +42,17 @@ public class FhirTesterMvcConfig extends WebMvcConfigurerAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SpringResourceTemplateResolver templateResolver() {
|
public SpringResourceTemplateResolver templateResolver(TesterConfig theTesterConfig) {
|
||||||
SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
|
SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
|
||||||
resolver.setPrefix("/WEB-INF/templates/");
|
resolver.setPrefix("/WEB-INF/templates/");
|
||||||
resolver.setSuffix(".html");
|
resolver.setSuffix(".html");
|
||||||
resolver.setTemplateMode(TemplateMode.HTML);
|
resolver.setTemplateMode(TemplateMode.HTML);
|
||||||
resolver.setCharacterEncoding("UTF-8");
|
resolver.setCharacterEncoding("UTF-8");
|
||||||
|
|
||||||
|
if (theTesterConfig.getDebugTemplatesMode()) {
|
||||||
|
resolver.setCacheable(false);
|
||||||
|
}
|
||||||
|
|
||||||
return resolver;
|
return resolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,17 +62,17 @@ public class FhirTesterMvcConfig extends WebMvcConfigurerAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public ThymeleafViewResolver viewResolver() {
|
public ThymeleafViewResolver viewResolver(SpringTemplateEngine theTemplateEngine) {
|
||||||
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
|
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
|
||||||
viewResolver.setTemplateEngine(templateEngine());
|
viewResolver.setTemplateEngine(theTemplateEngine);
|
||||||
viewResolver.setCharacterEncoding("UTF-8");
|
viewResolver.setCharacterEncoding("UTF-8");
|
||||||
return viewResolver;
|
return viewResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SpringTemplateEngine templateEngine() {
|
public SpringTemplateEngine templateEngine(SpringResourceTemplateResolver theTemplateResolver) {
|
||||||
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
|
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
|
||||||
templateEngine.setTemplateResolver(templateResolver());
|
templateEngine.setTemplateResolver(theTemplateResolver);
|
||||||
|
|
||||||
return templateEngine;
|
return templateEngine;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,35 @@
|
||||||
package ca.uhn.fhir.to;
|
package ca.uhn.fhir.to;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||||
import ca.uhn.fhir.i18n.Msg;
|
import ca.uhn.fhir.i18n.Msg;
|
||||||
import java.util.*;
|
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
||||||
|
import ca.uhn.fhir.rest.server.util.ITestingUiClientFactory;
|
||||||
import javax.annotation.PostConstruct;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.commons.lang3.Validate;
|
import org.apache.commons.lang3.Validate;
|
||||||
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
import org.springframework.beans.factory.annotation.Required;
|
import org.springframework.beans.factory.annotation.Required;
|
||||||
|
|
||||||
import ca.uhn.fhir.context.FhirVersionEnum;
|
import javax.annotation.PostConstruct;
|
||||||
import ca.uhn.fhir.rest.server.util.ITestingUiClientFactory;
|
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 {
|
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";
|
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<String, Boolean> myIdToAllowsApiKey = new LinkedHashMap<>();
|
||||||
|
private final LinkedHashMap<String, FhirVersionEnum> myIdToFhirVersion = new LinkedHashMap<>();
|
||||||
|
private final LinkedHashMap<String, String> myIdToServerBase = new LinkedHashMap<>();
|
||||||
|
private final LinkedHashMap<String, String> myIdToServerName = new LinkedHashMap<>();
|
||||||
|
private final List<ServerBuilder> myServerBuilders = new ArrayList<>();
|
||||||
|
private final LinkedHashMap<String, Map<String, IInclusionChecker>> myServerIdToTypeToOperationNameToInclusionChecker = new LinkedHashMap<>();
|
||||||
|
private final LinkedHashMap<String, Map<RestOperationTypeEnum, IInclusionChecker>> myServerIdToTypeToInteractionNameToInclusionChecker = new LinkedHashMap<>();
|
||||||
private ITestingUiClientFactory myClientFactory;
|
private ITestingUiClientFactory myClientFactory;
|
||||||
private LinkedHashMap<String, Boolean> myIdToAllowsApiKey = new LinkedHashMap<String, Boolean>();
|
|
||||||
private LinkedHashMap<String, FhirVersionEnum> myIdToFhirVersion = new LinkedHashMap<String, FhirVersionEnum>();
|
|
||||||
private LinkedHashMap<String, String> myIdToServerBase = new LinkedHashMap<String, String>();
|
|
||||||
private LinkedHashMap<String, String> myIdToServerName = new LinkedHashMap<String, String>();
|
|
||||||
private boolean myRefuseToFetchThirdPartyUrls = true;
|
private boolean myRefuseToFetchThirdPartyUrls = true;
|
||||||
private List<ServerBuilder> myServerBuilders = new ArrayList<TesterConfig.ServerBuilder>();
|
private boolean myDebugTemplatesMode;
|
||||||
|
|
||||||
public IServerBuilderStep1 addServer() {
|
public IServerBuilderStep1 addServer() {
|
||||||
ServerBuilder retVal = new ServerBuilder();
|
ServerBuilder retVal = new ServerBuilder();
|
||||||
|
@ -42,6 +48,11 @@ public class TesterConfig {
|
||||||
myIdToServerBase.put(next.myId, next.myBaseUrl);
|
myIdToServerBase.put(next.myId, next.myBaseUrl);
|
||||||
myIdToServerName.put(next.myId, next.myName);
|
myIdToServerName.put(next.myId, next.myName);
|
||||||
myIdToAllowsApiKey.put(next.myId, next.myAllowsApiKey);
|
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();
|
myServerBuilders.clear();
|
||||||
}
|
}
|
||||||
|
@ -50,8 +61,12 @@ public class TesterConfig {
|
||||||
return myClientFactory;
|
return myClientFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setClientFactory(ITestingUiClientFactory theClientFactory) {
|
||||||
|
myClientFactory = theClientFactory;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean getDebugTemplatesMode() {
|
public boolean getDebugTemplatesMode() {
|
||||||
return true;
|
return myDebugTemplatesMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LinkedHashMap<String, Boolean> getIdToAllowsApiKey() {
|
public LinkedHashMap<String, Boolean> getIdToAllowsApiKey() {
|
||||||
|
@ -72,24 +87,52 @@ public class TesterConfig {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If set to {@literal true} (default is true) the server will refuse to load URLs in
|
* 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() {
|
public boolean isRefuseToFetchThirdPartyUrls() {
|
||||||
return myRefuseToFetchThirdPartyUrls;
|
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
|
* 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) {
|
public void setRefuseToFetchThirdPartyUrls(boolean theRefuseToFetchThirdPartyUrls) {
|
||||||
myRefuseToFetchThirdPartyUrls = theRefuseToFetchThirdPartyUrls;
|
myRefuseToFetchThirdPartyUrls = theRefuseToFetchThirdPartyUrls;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called from Thymeleaf
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public List<String> getSearchResultRowOperations(String theId, IIdType theResourceId) {
|
||||||
|
List<String> retVal = new ArrayList<>();
|
||||||
|
|
||||||
|
Map<String, IInclusionChecker> 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<String> retVal = new ArrayList<>();
|
||||||
|
|
||||||
|
Map<RestOperationTypeEnum, IInclusionChecker> 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
|
@Required
|
||||||
public void setServers(List<String> theServers) {
|
public void setServers(List<String> theServers) {
|
||||||
List<String> servers = theServers;
|
List<String> servers = theServers;
|
||||||
|
@ -97,7 +140,7 @@ public class TesterConfig {
|
||||||
// This is mostly for unit tests
|
// This is mostly for unit tests
|
||||||
String force = System.getProperty(SYSPROP_FORCE_SERVERS);
|
String force = System.getProperty(SYSPROP_FORCE_SERVERS);
|
||||||
if (StringUtils.isNotBlank(force)) {
|
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);
|
servers = Collections.singletonList(force);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,15 +191,49 @@ public class TesterConfig {
|
||||||
|
|
||||||
IServerBuilderStep5 allowsApiKey();
|
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 {
|
public class ServerBuilder implements IServerBuilderStep1, IServerBuilderStep2, IServerBuilderStep3, IServerBuilderStep4, IServerBuilderStep5 {
|
||||||
|
|
||||||
|
private final Map<String, IInclusionChecker> myOperationNameToInclusionChecker = new LinkedHashMap<>();
|
||||||
|
private final Map<RestOperationTypeEnum, IInclusionChecker> mySearchResultRowInteractionEnabled = new LinkedHashMap<>();
|
||||||
private boolean myAllowsApiKey;
|
private boolean myAllowsApiKey;
|
||||||
private String myBaseUrl;
|
private String myBaseUrl;
|
||||||
private String myId;
|
private String myId;
|
||||||
private String myName;
|
private String myName;
|
||||||
private FhirVersionEnum myVersion;
|
private FhirVersionEnum myVersion;
|
||||||
|
private boolean myEnableDebugTemplates;
|
||||||
|
|
||||||
|
public ServerBuilder() {
|
||||||
|
mySearchResultRowInteractionEnabled.put(RestOperationTypeEnum.READ, id -> true);
|
||||||
|
mySearchResultRowInteractionEnabled.put(RestOperationTypeEnum.UPDATE, id -> true);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IServerBuilderStep1 addServer() {
|
public IServerBuilderStep1 addServer() {
|
||||||
|
@ -171,6 +248,24 @@ public class TesterConfig {
|
||||||
return this;
|
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
|
@Override
|
||||||
public IServerBuilderStep4 withBaseUrl(String theBaseUrl) {
|
public IServerBuilderStep4 withBaseUrl(String theBaseUrl) {
|
||||||
Validate.notBlank(theBaseUrl, "theBaseUrl can not be blank");
|
Validate.notBlank(theBaseUrl, "theBaseUrl can not be blank");
|
||||||
|
@ -200,5 +295,4 @@ public class TesterConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,246 +117,29 @@
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr th:if="${!#strings.isEmpty(narrative)}">
|
||||||
|
<td class="propertyKeyCell">Result Narrative</td>
|
||||||
|
<td th:utext="${narrative}"></td>
|
||||||
|
</tr>
|
||||||
<tr th:if="${!#strings.isEmpty(resultBody)}">
|
<tr th:if="${!#strings.isEmpty(resultBody)}">
|
||||||
<td rowspan="2">
|
<td rowspan="2">
|
||||||
Result Body
|
Result Body
|
||||||
<small th:text="${resultDescription}" style="font-weight: normal;"/>
|
<small th:text="${resultDescription}" style="font-weight: normal;"/>
|
||||||
</td>
|
</td>
|
||||||
<td style="border-width: 0px; padding: 0px;">
|
<td style="border-width: 0px; padding: 0px;">
|
||||||
|
|
||||||
<!--
|
|
||||||
If the response is a bundle, this block will contain a collapsible
|
|
||||||
table with a summary of each entry as well as paging buttons and
|
|
||||||
controls for viewing/editing/etc results
|
|
||||||
|
|
||||||
NON-RI Bundle
|
|
||||||
-->
|
|
||||||
<div th:if="${bundle} != null" class="panel-group" id="accordion" style="margin-bottom: 0px;">
|
|
||||||
<div class="panel panel-default" style="border: none; border-bottom: 1px solid #ddd; border-radius: 0px;">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<div class="panel-title">
|
|
||||||
<th:block th:if="${#lists.isEmpty(bundle.entries)}">Bundle contains no entries</th:block>
|
|
||||||
<a th:unless="${#lists.isEmpty(bundle.entries)}" data-toggle="collapse" data-parent="#accordion" href="#collapseOne">
|
|
||||||
<i id="collapseOneIcon" class="far fa-minus-square"></i>
|
|
||||||
<span th:if="${bundle.totalResults.empty}" th:text="'Bundle contains ' + ${#lists.size(bundle.entries)} + ' entries'"/>
|
|
||||||
<span th:unless="${bundle.totalResults.empty}" th:text="'Bundle contains ' + ${#lists.size(bundle.entries)} + ' / ' + ${bundle.totalResults.value} + ' entries'"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<th:block th:if="${!bundle.linkNext.empty} or ${!bundle.linkPrevious.empty}">
|
<th:block th:if="${bundle} != null">
|
||||||
|
<th:block th:replace="tmpl-result-controltable-hapi :: controltable"></th:block>
|
||||||
<!-- Prev/Next Page Buttons -->
|
</th:block>
|
||||||
<button class="btn btn-success btn-xs" type="button" id="page-prev-btn"
|
<th:block th:if="${riBundle} != null AND ( ${riBundle.type.name()} == 'SEARCHSET' OR ${riBundle.type.name()} == 'HISTORY' )">
|
||||||
style="margin-left: 15px;">
|
<th:block th:replace="tmpl-result-controltable-ri :: controltable"></th:block>
|
||||||
<i class="fas fa-angle-double-left"></i>
|
</th:block>
|
||||||
Prev Page
|
|
||||||
</button>
|
<div class="panel-heading">
|
||||||
<script type="text/javascript" th:inline="javascript">
|
<div class="panel-title-text">
|
||||||
if ([[${bundle.linkPrevious.empty}]]) {
|
Payload
|
||||||
$('#page-prev-btn').prop('disabled', true);
|
|
||||||
}
|
|
||||||
$('#page-prev-btn').click(function() {
|
|
||||||
var btn = $(this);
|
|
||||||
handleActionButtonClick($(this));
|
|
||||||
btn.append($('<input />', { type: 'hidden', name: 'page-url', value: [[${bundle.linkPrevious.value}]] }));
|
|
||||||
$("#outerForm").attr("action", "page").submit();
|
|
||||||
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<button class="btn btn-success btn-xs" type="button" id="page-next-btn">
|
|
||||||
<i class="fas fa-angle-double-right"></i>
|
|
||||||
Next Page
|
|
||||||
</button>
|
|
||||||
<script type="text/javascript" th:inline="javascript">
|
|
||||||
if ([[${bundle.linkNext.empty}]]) {
|
|
||||||
$('#page-next-btn').prop('disabled', true);
|
|
||||||
}
|
|
||||||
$('#page-next-btn').click(function() {
|
|
||||||
var btn = $(this);
|
|
||||||
handleActionButtonClick($(this));
|
|
||||||
btn.append($('<input />', { type: 'hidden', name: 'page-url', value: [[${bundle.linkNext.value}]] }));
|
|
||||||
$("#outerForm").attr("action", "page").submit();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</th:block>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="collapseOne" class="panel-collapse in" th:unless="${#lists.isEmpty(bundle.entries)}">
|
|
||||||
<div class="panel-body" style="padding-bottom: 0px;">
|
|
||||||
<table class="table table-condensed" style="padding-bottom: 0px; margin-bottom: 0px;">
|
|
||||||
<colgroup>
|
|
||||||
<col style="width: 100px;"/>
|
|
||||||
<col/>
|
|
||||||
<col/>
|
|
||||||
<col style="width: 100px;"/>
|
|
||||||
</colgroup>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>Title</th>
|
|
||||||
<th>Updated</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr th:each="entry : ${bundle.entries}">
|
|
||||||
<td style="white-space: nowrap;">
|
|
||||||
<th:block th:if="${entry.resource} != null">
|
|
||||||
<button class="btn btn-primary btn-xs" th:data1="${entry.resource.id.resourceType}" th:data2="${entry.resource.id.idPart}" th:data3="${#strings.defaultString(entry.resource.id.versionIdPart,'')}" onclick="readFromEntriesTable(this, this.getAttribute('data1'), this.getAttribute('data2'), this.getAttribute('data3'));" type="submit" name="action" value="read"><i class="fas fa-book"></i> Read</button>
|
|
||||||
<button class="btn btn-primary btn-xs" th:data1="${entry.resource.id.resourceType}" th:data2="${entry.resource.id.idPart}" th:data3="${#strings.defaultString(entry.resource.id.versionIdPart,'')}" onclick="updateFromEntriesTable(this, this.getAttribute('data1'), this.getAttribute('data2'), this.getAttribute('data3'));" type="submit" name="action" value="home"><i class="far fa-edit"></i> Update</button>
|
|
||||||
</th:block>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a th:if="${entry.resource} != null" th:href="${entry.resource.id}" th:text="${entry.resource.id.toUnqualified()}" style="font-size: 0.8em"/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<!-- Title used to go here -->
|
|
||||||
</td>
|
|
||||||
<td th:if="${entry.updated.value} == null"></td>
|
|
||||||
<td th:if="${entry.updated.value} != null and ${entry.updated.today} == true" th:text="${#dates.format(entry.updated.value, 'HH:mm:ss')}"></td>
|
|
||||||
<td th:if="${entry.updated.value} != null and ${entry.updated.today} == false" th:text="${#dates.format(entry.updated.value, 'yyyy-MM-dd')}"></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script type="text/javascript">
|
|
||||||
/*
|
|
||||||
$('#collapseOne').on('hidden.bs.collapse', function () {
|
|
||||||
$("#collapseOneIcon").removeClass("fa-minus-square").addClass("fa-plus-square");
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#collapseOne').on('shown.bs.collapse', function () {
|
|
||||||
$("#collapseOneIcon").removeClass("fa-plus-square").addClass("fa-minus-square");
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
</script>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- END Non-RI Bundle -->
|
|
||||||
|
|
||||||
|
|
||||||
<!--
|
|
||||||
If the response is a bundle, this block will contain a collapsible
|
|
||||||
table with a summary of each entry as well as paging buttons and
|
|
||||||
controls for viewing/editing/etc results
|
|
||||||
|
|
||||||
RI Bundle
|
|
||||||
-->
|
|
||||||
<div th:if="${riBundle} != null" class="panel-group" id="accordion" style="margin-bottom: 0px;">
|
|
||||||
<div class="panel panel-default" style="border: none; border-bottom: 1px solid #ddd; border-radius: 0px;">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<div class="panel-title">
|
|
||||||
<th:block th:if="${#lists.isEmpty(riBundle.entry)}">Bundle contains no entries</th:block>
|
|
||||||
<a th:unless="${#lists.isEmpty(riBundle.entry)}" data-toggle="collapse" data-parent="#accordion" href="#collapseOne">
|
|
||||||
<i id="collapseOneIcon" class="far fa-minus-square"></i>
|
|
||||||
<span th:if="${riBundle.totalElement.empty}" th:text="'Bundle contains ' + ${#lists.size(riBundle.entry)} + ' entries'"/>
|
|
||||||
<span th:unless="${riBundle.totalElement.empty}" th:text="'Bundle contains ' + ${#lists.size(riBundle.entry)} + ' / ' + ${riBundle.totalElement.value} + ' entries'"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<th:block th:if="${riBundle.getLink('next') != null} or ${riBundle.getLink('prev') != null} or ${riBundle.getLink('previous') != null}">
|
|
||||||
|
|
||||||
<!-- Prev/Next Page Buttons -->
|
|
||||||
<button class="btn btn-success btn-xs" type="button" id="page-prev-btn"
|
|
||||||
style="margin-left: 15px;">
|
|
||||||
<i class="fas fa-angle-double-left"></i>
|
|
||||||
Prev Page
|
|
||||||
</button>
|
|
||||||
<script type="text/javascript" th:inline="javascript">
|
|
||||||
if ([[${riBundle.getLink('prev') == null && riBundle.getLink('previous') == null}]]) {
|
|
||||||
$('#page-prev-btn').prop('disabled', true);
|
|
||||||
}
|
|
||||||
$('#page-prev-btn').click(function() {
|
|
||||||
var btn = $(this);
|
|
||||||
handleActionButtonClick($(this));
|
|
||||||
var prev = [[${riBundle.getLinkOrCreate('prev').url}]];
|
|
||||||
var previous = [[${riBundle.getLinkOrCreate('previous').url}]];
|
|
||||||
var url = prev != null ? prev : previous;
|
|
||||||
btn.append($('<input />', { type: 'hidden', name: 'page-url', value: url }));
|
|
||||||
$("#outerForm").attr("action", "page").submit();
|
|
||||||
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<button class="btn btn-success btn-xs" type="button" id="page-next-btn">
|
|
||||||
<i class="fas fa-angle-double-right"></i>
|
|
||||||
Next Page
|
|
||||||
</button>
|
|
||||||
<script type="text/javascript" th:inline="javascript">
|
|
||||||
if ([[${riBundle.getLink('next') == null}]]) {
|
|
||||||
$('#page-next-btn').prop('disabled', true);
|
|
||||||
}
|
|
||||||
$('#page-next-btn').click(function() {
|
|
||||||
var btn = $(this);
|
|
||||||
handleActionButtonClick($(this));
|
|
||||||
btn.append($('<input />', { type: 'hidden', name: 'page-url', value: [[${riBundle.getLinkOrCreate('next').url}]] }));
|
|
||||||
$("#outerForm").attr("action", "page").submit();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</th:block>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="collapseOne" class="panel-collapse in" th:unless="${#lists.isEmpty(riBundle.entry)}">
|
|
||||||
<div class="panel-body" style="padding-bottom: 0px;">
|
|
||||||
<table class="table table-condensed" style="padding-bottom: 0px; margin-bottom: 0px;">
|
|
||||||
<colgroup>
|
|
||||||
<col style="width: 100px;"/>
|
|
||||||
<col/>
|
|
||||||
<col/>
|
|
||||||
<col style="width: 100px;"/>
|
|
||||||
</colgroup>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>Updated</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr th:each="entry : ${riBundle.entry}">
|
|
||||||
<td style="white-space: nowrap;">
|
|
||||||
<th:block th:if="${entry.resource} != null">
|
|
||||||
<button class="btn btn-primary btn-xs" th:data1="${entry.resource.idElement.resourceType}" th:data2="${entry.resource.idElement.idPart}" th:data3="${#strings.defaultString(entry.resource.idElement.versionIdPart,'')}" onclick="readFromEntriesTable(this, this.getAttribute('data1'), this.getAttribute('data2'), this.getAttribute('data3'));" type="submit" name="action" value="read"><i class="fas fa-book"></i> Read</button>
|
|
||||||
<button class="btn btn-primary btn-xs" th:data1="${entry.resource.idElement.resourceType}" th:data2="${entry.resource.idElement.idPart}" th:data3="${#strings.defaultString(entry.resource.idElement.versionIdPart,'')}" onclick="updateFromEntriesTable(this, this.getAttribute('data1'), this.getAttribute('data2'), this.getAttribute('data3'));" type="submit" name="action" value="home"><i class="far fa-edit"></i> Update</button>
|
|
||||||
</th:block>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a th:if="${entry.resource} != null" th:href="${entry.resource.id}" th:text="${entry.resource.idElement.toUnqualified()}" style="font-size: 0.8em"/>
|
|
||||||
</td>
|
|
||||||
<th:block th:if="${ri}">
|
|
||||||
<td th:if="${entry.resource} == null or ${entry.resource.meta.lastUpdatedElement.value} == null"></td>
|
|
||||||
<td th:if="${entry.resource} != null and ${entry.resource.meta.lastUpdatedElement.value} != null and ${entry.resource.meta.lastUpdatedElement.today} == true" th:text="${#dates.format(entry.resource.meta.lastUpdated, 'HH:mm:ss')}"></td>
|
|
||||||
<td th:if="${entry.resource} != null and ${entry.resource.meta.lastUpdatedElement.value} != null and ${entry.resource.meta.lastUpdatedElement.today} == false" th:text="${#dates.format(entry.resource.meta.lastUpdated, 'yyyy-MM-dd HH:mm:ss')}"></td>
|
|
||||||
</th:block>
|
|
||||||
<th:block th:unless="${ri}">
|
|
||||||
<td></td>
|
|
||||||
</th:block>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script type="text/javascript">
|
|
||||||
/*
|
|
||||||
$('#collapseOne').on('hidden.bs.collapse', function () {
|
|
||||||
$("#collapseOneIcon").removeClass("fa-minus-square").addClass("fa-plus-square");
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#collapseOne').on('shown.bs.collapse', function () {
|
|
||||||
$("#collapseOneIcon").removeClass("fa-plus-square").addClass("fa-minus-square");
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
</script>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- END RI Bundle -->
|
|
||||||
|
|
||||||
|
|
||||||
<div class="panel-heading" sstyle="margin: 5px;">
|
|
||||||
<h4 class="panel-title">
|
|
||||||
Raw Message
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr th:if="${!#strings.isEmpty(resultBody)}">
|
<tr th:if="${!#strings.isEmpty(resultBody)}">
|
||||||
|
@ -367,10 +150,6 @@
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr th:if="${!#strings.isEmpty(narrative)}">
|
|
||||||
<td class="propertyKeyCell">Result Narrative</td>
|
|
||||||
<td th:utext="${narrative}"></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
<!--/*
|
||||||
|
If the response is a bundle, this block will contain a collapsible
|
||||||
|
table with a summary of each entry as well as paging buttons and
|
||||||
|
controls for viewing/editing/etc results
|
||||||
|
|
||||||
|
ca.uhn.hapi Bundle
|
||||||
|
*/-->
|
||||||
|
<th:block th:fragment="controltable">
|
||||||
|
<div class="panel-group" id="accordion" style="margin-bottom: 0px;">
|
||||||
|
<div class="panel panel-default" style="border: none; border-bottom: 1px solid #ddd; border-radius: 0px;">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<div class="panel-title">
|
||||||
|
<th:block th:if="${#lists.isEmpty(bundle.entries)}">Bundle contains no entries</th:block>
|
||||||
|
<a th:unless="${#lists.isEmpty(bundle.entries)}" data-toggle="collapse" data-parent="#accordion" href="#collapseOne">
|
||||||
|
<i id="collapseOneIcon" class="far fa-minus-square"></i>
|
||||||
|
<span th:if="${bundle.totalResults.empty}" th:text="'Bundle contains ' + ${#lists.size(bundle.entries)} + ' entries'"/>
|
||||||
|
<span th:unless="${bundle.totalResults.empty}" th:text="'Bundle contains ' + ${#lists.size(bundle.entries)} + ' / ' + ${bundle.totalResults.value} + ' entries'"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<th:block th:if="${!bundle.linkNext.empty} or ${!bundle.linkPrevious.empty}">
|
||||||
|
|
||||||
|
<!-- Prev/Next Page Buttons -->
|
||||||
|
<button class="btn btn-success btn-xs" type="button" id="page-prev-btn"
|
||||||
|
style="margin-left: 15px;">
|
||||||
|
<i class="fas fa-angle-double-left"></i>
|
||||||
|
Prev Page
|
||||||
|
</button>
|
||||||
|
<script type="text/javascript" th:inline="javascript">
|
||||||
|
if ([[${bundle.linkPrevious.empty}]]) {
|
||||||
|
$('#page-prev-btn').prop('disabled', true);
|
||||||
|
}
|
||||||
|
$('#page-prev-btn').click(function() {
|
||||||
|
var btn = $(this);
|
||||||
|
handleActionButtonClick($(this));
|
||||||
|
btn.append($('<input />', { type: 'hidden', name: 'page-url', value: [[${bundle.linkPrevious.value}]] }));
|
||||||
|
$("#outerForm").attr("action", "page").submit();
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button class="btn btn-success btn-xs" type="button" id="page-next-btn">
|
||||||
|
<i class="fas fa-angle-double-right"></i>
|
||||||
|
Next Page
|
||||||
|
</button>
|
||||||
|
<script type="text/javascript" th:inline="javascript">
|
||||||
|
if ([[${bundle.linkNext.empty}]]) {
|
||||||
|
$('#page-next-btn').prop('disabled', true);
|
||||||
|
}
|
||||||
|
$('#page-next-btn').click(function() {
|
||||||
|
var btn = $(this);
|
||||||
|
handleActionButtonClick($(this));
|
||||||
|
btn.append($('<input />', { type: 'hidden', name: 'page-url', value: [[${bundle.linkNext.value}]] }));
|
||||||
|
$("#outerForm").attr("action", "page").submit();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="collapseOne" class="panel-collapse in" th:unless="${#lists.isEmpty(bundle.entries)}">
|
||||||
|
<div class="panel-body" style="padding-bottom: 0px;">
|
||||||
|
<table class="table table-condensed" style="padding-bottom: 0px; margin-bottom: 0px;">
|
||||||
|
<colgroup>
|
||||||
|
<col style="width: 100px;"/>
|
||||||
|
<col/>
|
||||||
|
<col/>
|
||||||
|
<col style="width: 100px;"/>
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Updated</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="entry : ${bundle.entries}">
|
||||||
|
<td style="white-space: nowrap;">
|
||||||
|
<th:block th:if="${entry.resource} != null">
|
||||||
|
<button class="btn btn-primary btn-xs" th:data1="${entry.resource.id.resourceType}" th:data2="${entry.resource.id.idPart}" th:data3="${#strings.defaultString(entry.resource.id.versionIdPart,'')}" onclick="readFromEntriesTable(this, this.getAttribute('data1'), this.getAttribute('data2'), this.getAttribute('data3'));" type="submit" name="action" value="read"><i class="fas fa-book"></i> Read</button>
|
||||||
|
<button class="btn btn-primary btn-xs" th:data1="${entry.resource.id.resourceType}" th:data2="${entry.resource.id.idPart}" th:data3="${#strings.defaultString(entry.resource.id.versionIdPart,'')}" onclick="updateFromEntriesTable(this, this.getAttribute('data1'), this.getAttribute('data2'), this.getAttribute('data3'));" type="submit" name="action" value="home"><i class="far fa-edit"></i> Update</button>
|
||||||
|
</th:block>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a th:if="${entry.resource} != null" th:href="${entry.resource.id}" th:text="${entry.resource.id.toUnqualified()}" style="font-size: 0.8em"/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<!-- Title used to go here -->
|
||||||
|
</td>
|
||||||
|
<td th:if="${entry.updated.value} == null"></td>
|
||||||
|
<td th:if="${entry.updated.value} != null and ${entry.updated.today} == true" th:text="${#dates.format(entry.updated.value, 'HH:mm:ss')}"></td>
|
||||||
|
<td th:if="${entry.updated.value} != null and ${entry.updated.today} == false" th:text="${#dates.format(entry.updated.value, 'yyyy-MM-dd')}"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
/*
|
||||||
|
$('#collapseOne').on('hidden.bs.collapse', function () {
|
||||||
|
$("#collapseOneIcon").removeClass("fa-minus-square").addClass("fa-plus-square");
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#collapseOne').on('shown.bs.collapse', function () {
|
||||||
|
$("#collapseOneIcon").removeClass("fa-plus-square").addClass("fa-minus-square");
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- END Non-RI Bundle -->
|
||||||
|
</th:block>
|
|
@ -0,0 +1,105 @@
|
||||||
|
<!--/*
|
||||||
|
If the response is a bundle, this block will contain a collapsible
|
||||||
|
table with a summary of each entry as well as paging buttons and
|
||||||
|
controls for viewing/editing/etc results
|
||||||
|
|
||||||
|
RI Bundle
|
||||||
|
*/-->
|
||||||
|
<th:block th:fragment="controltable">
|
||||||
|
<div class="panel-group" id="accordion" style="margin-bottom: 0px;">
|
||||||
|
<div class="panel panel-default" style="border: none; border-bottom: 1px solid #ddd; border-radius: 0px;">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<div class="panel-title">
|
||||||
|
|
||||||
|
<div class="panel-title-text">
|
||||||
|
<th:block th:if="${#lists.isEmpty(riBundle.entry)}">Bundle contains no entries</th:block>
|
||||||
|
<th:block th:unless="${#lists.isEmpty(riBundle.entry)}">
|
||||||
|
<th:block th:if="${riBundle.totalElement.empty}" th:text="'Bundle contains ' + ${#lists.size(riBundle.entry)} + ' entries'"/>
|
||||||
|
<th:block th:unless="${riBundle.totalElement.empty}" th:text="'Bundle contains ' + ${#lists.size(riBundle.entry)} + ' / ' + ${riBundle.totalElement.value} + ' entries'"/>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<th:block th:if="${riBundle.getLink('next') != null} or ${riBundle.getLink('prev') != null} or ${riBundle.getLink('previous') != null}">
|
||||||
|
|
||||||
|
<!-- Prev/Next Page Buttons -->
|
||||||
|
<button class="btn btn-success btn-xs" type="button" id="page-prev-btn"
|
||||||
|
style="margin-left: 15px;">
|
||||||
|
<i class="fas fa-angle-double-left"></i>
|
||||||
|
Prev Page
|
||||||
|
</button>
|
||||||
|
<script type="text/javascript" th:inline="javascript">
|
||||||
|
if ([[${riBundle.getLink('prev') == null && riBundle.getLink('previous') == null}]]) {
|
||||||
|
$('#page-prev-btn').prop('disabled', true);
|
||||||
|
}
|
||||||
|
$('#page-prev-btn').click(function() {
|
||||||
|
var btn = $(this);
|
||||||
|
handleActionButtonClick($(this));
|
||||||
|
var prev = [[${riBundle.getLinkOrCreate('prev').url}]];
|
||||||
|
var previous = [[${riBundle.getLinkOrCreate('previous').url}]];
|
||||||
|
var url = prev != null ? prev : previous;
|
||||||
|
btn.append($('<input />', { type: 'hidden', name: 'page-url', value: url }));
|
||||||
|
$("#outerForm").attr("action", "page").submit();
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button class="btn btn-success btn-xs" type="button" id="page-next-btn">
|
||||||
|
<i class="fas fa-angle-double-right"></i>
|
||||||
|
Next Page
|
||||||
|
</button>
|
||||||
|
<script type="text/javascript" th:inline="javascript">
|
||||||
|
if ([[${riBundle.getLink('next') == null}]]) {
|
||||||
|
$('#page-next-btn').prop('disabled', true);
|
||||||
|
}
|
||||||
|
$('#page-next-btn').click(function() {
|
||||||
|
var btn = $(this);
|
||||||
|
handleActionButtonClick($(this));
|
||||||
|
btn.append($('<input />', { type: 'hidden', name: 'page-url', value: [[${riBundle.getLinkOrCreate('next').url}]] }));
|
||||||
|
$("#outerForm").attr("action", "page").submit();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</th:block>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="collapseOne" class="panel-collapse in" th:unless="${#lists.isEmpty(riBundle.entry)}">
|
||||||
|
<div class="panel-body" style="padding-bottom: 0px;">
|
||||||
|
<table class="table table-condensed" id="resultControlsTable" style="padding-bottom: 0px; margin-bottom: 0px; width: 100%;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Updated</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="entry : ${riBundle.entry}">
|
||||||
|
<td class="resultControlButtons">
|
||||||
|
<th:block th:if="${entry.resource} != null">
|
||||||
|
<!-- Individual Search Result Button Row -->
|
||||||
|
<button th:if="${config.isSearchResultRowInteractionEnabled(serverId, 'read', entry.resource.idElement)}" class="btn btn-primary btn-xs" th:data1="${entry.resource.idElement.resourceType}" th:data2="${entry.resource.idElement.idPart}" th:data3="${#strings.defaultString(entry.resource.idElement.versionIdPart,'')}" onclick="readFromEntriesTable(this, this.getAttribute('data1'), this.getAttribute('data2'), this.getAttribute('data3'));" type="submit" name="action" value="read"><i class="fas fa-book"></i> Read</button>
|
||||||
|
<button th:if="${config.isSearchResultRowInteractionEnabled(serverId, 'update', entry.resource.idElement)}" class="btn btn-primary btn-xs" th:data1="${entry.resource.idElement.resourceType}" th:data2="${entry.resource.idElement.idPart}" th:data3="${#strings.defaultString(entry.resource.idElement.versionIdPart,'')}" onclick="updateFromEntriesTable(this, this.getAttribute('data1'), this.getAttribute('data2'), this.getAttribute('data3'));" type="submit" name="action" value="home"><i class="far fa-edit"></i> Update</button>
|
||||||
|
<th:block th:each="operation : ${config.getSearchResultRowOperations(serverId, entry.resource.idElement)}">
|
||||||
|
<button class="btn btn-primary btn-xs" th:data1="${entry.resource.idElement.resourceType}" th:data2="${entry.resource.idElement.idPart}" th:data3="${operation}" onclick="executeOperation(this, this.getAttribute('data1'), this.getAttribute('data2'), this.getAttribute('data3'));" type="submit" name="action" value="home">[[${operation}]]</button>
|
||||||
|
</th:block>
|
||||||
|
</th:block>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a th:if="${entry.resource} != null" th:href="${entry.resource.id}" th:text="${entry.resource.idElement.toUnqualified()}" style="font-size: 0.8em"/>
|
||||||
|
</td>
|
||||||
|
<th:block th:if="${ri}">
|
||||||
|
<td th:if="${entry.resource} == null or ${entry.resource.meta.lastUpdatedElement.value} == null"></td>
|
||||||
|
<td th:if="${entry.resource} != null and ${entry.resource.meta.lastUpdatedElement.value} != null and ${entry.resource.meta.lastUpdatedElement.today} == true" th:text="${#dates.format(entry.resource.meta.lastUpdated, 'HH:mm:ss')}"></td>
|
||||||
|
<td th:if="${entry.resource} != null and ${entry.resource.meta.lastUpdatedElement.value} != null and ${entry.resource.meta.lastUpdatedElement.today} == false" th:text="${#dates.format(entry.resource.meta.lastUpdated, 'yyyy-MM-dd HH:mm:ss')}"></td>
|
||||||
|
</th:block>
|
||||||
|
<th:block th:unless="${ri}">
|
||||||
|
<td></td>
|
||||||
|
</th:block>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- END RI Bundle -->
|
||||||
|
</th:block>
|
|
@ -26,6 +26,19 @@ body {
|
||||||
font-family: sans-serif;
|
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 {
|
.clientCodeBox {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
|
@ -53,6 +66,14 @@ label {
|
||||||
line-height: 0.8em;
|
line-height: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TD.resultControlButtons {
|
||||||
|
font-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TD.resultControlButtons BUTTON {
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
TD.headerBox {
|
TD.headerBox {
|
||||||
line-height: 0.8em !important;
|
line-height: 0.8em !important;
|
||||||
}
|
}
|
||||||
|
@ -153,10 +174,12 @@ DIV.navbarBreadcrumb:HOVER, A.navbarBreadcrumb:HOVER {
|
||||||
}
|
}
|
||||||
|
|
||||||
DIV.resultBodyActual {
|
DIV.resultBodyActual {
|
||||||
/*
|
padding: 5px;
|
||||||
max-height: 400px;
|
}
|
||||||
overflow: scroll;
|
|
||||||
*/
|
DIV.panel-title-text {
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
PRE.resultBodyPre {
|
PRE.resultBodyPre {
|
||||||
|
|
|
@ -507,8 +507,20 @@ function updateFromEntriesTable(source, type, id, vid) {
|
||||||
}
|
}
|
||||||
setResource(btn, type);
|
setResource(btn, type);
|
||||||
$("#outerForm").attr("action", "resource").submit();
|
$("#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($('<input />', { type: 'hidden', name: 'instanceType', value: type }));
|
||||||
|
btn.append($('<input />', { type: 'hidden', name: 'instanceId', value: id }));
|
||||||
|
btn.append($('<input />', { type: 'hidden', name: 'operationName', value: operationName }));
|
||||||
|
$("#outerForm").attr("action", "operation").submit();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* http://stackoverflow.com/a/10997390/11236
|
* http://stackoverflow.com/a/10997390/11236
|
||||||
|
|
|
@ -8,7 +8,6 @@ import ca.uhn.fhir.context.FhirVersionEnum;
|
||||||
import ca.uhn.fhir.to.FhirTesterMvcConfig;
|
import ca.uhn.fhir.to.FhirTesterMvcConfig;
|
||||||
import ca.uhn.fhir.to.TesterConfig;
|
import ca.uhn.fhir.to.TesterConfig;
|
||||||
|
|
||||||
//@formatter:off
|
|
||||||
/**
|
/**
|
||||||
* This spring config file configures the web testing module. It serves two
|
* This spring config file configures the web testing module. It serves two
|
||||||
* purposes:
|
* purposes:
|
||||||
|
@ -24,15 +23,15 @@ public class FhirTesterConfig {
|
||||||
/**
|
/**
|
||||||
* This bean tells the testing webpage which servers it should configure itself
|
* 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
|
* 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
|
* 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
|
* deploy somewhere else, you might choose to only put your own server's
|
||||||
* address here.
|
* address here.
|
||||||
*
|
*
|
||||||
* Note the use of the ${serverBase} variable below. This will be replaced with
|
* 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
|
* the base URL as reported by the server itself. Often for a simple Tomcat
|
||||||
* (or other container) installation, this will end up being something
|
* (or other container) installation, this will end up being something
|
||||||
* like "http://localhost:8080/hapi-fhir-jpaserver-example". If you are
|
* 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.
|
* you might want to use that instead of using the variable.
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
|
@ -41,7 +40,7 @@ public class FhirTesterConfig {
|
||||||
retVal
|
retVal
|
||||||
.addServer()
|
.addServer()
|
||||||
.withId("internal")
|
.withId("internal")
|
||||||
.withFhirVersion(FhirVersionEnum.DSTU2)
|
.withFhirVersion(FhirVersionEnum.R4)
|
||||||
.withBaseUrl("http://localhost:8888/fhir")
|
.withBaseUrl("http://localhost:8888/fhir")
|
||||||
.withName("Localhost Server")
|
.withName("Localhost Server")
|
||||||
.allowsApiKey()
|
.allowsApiKey()
|
||||||
|
@ -63,6 +62,5 @@ public class FhirTesterConfig {
|
||||||
.withName("Local Tester");
|
.withName("Local Tester");
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
//@formatter:on
|
|
||||||
|
|
|
@ -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<Patient> 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<HtmlTableRow> 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<HtmlTableRow> 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<HtmlTableRow> 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<HtmlTableRow> 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.<HtmlAnchor>getHtmlElementById("leftResourcePatient").click();
|
||||||
|
// Click search button
|
||||||
|
HtmlPage searchResultPage = patientPage.<HtmlButton>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("<div>HELLO WORLD DOCUMENT</div>");
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
|
||||||
|
|
||||||
|
<!-- Creates the Spring Container shared by all Servlets and Filters -->
|
||||||
|
<listener>
|
||||||
|
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
|
||||||
|
</listener>
|
||||||
|
<context-param>
|
||||||
|
<param-name>contextClass</param-name>
|
||||||
|
<param-value>
|
||||||
|
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
|
||||||
|
</param-value>
|
||||||
|
</context-param>
|
||||||
|
<context-param>
|
||||||
|
<param-name>contextConfigLocation</param-name>
|
||||||
|
<param-value>
|
||||||
|
ca.uhn.fhir.jpa.test.FhirServerConfig
|
||||||
|
</param-value>
|
||||||
|
</context-param>
|
||||||
|
|
||||||
|
<servlet>
|
||||||
|
<servlet-name>spring</servlet-name>
|
||||||
|
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
|
||||||
|
<init-param>
|
||||||
|
<param-name>contextClass</param-name>
|
||||||
|
<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
|
||||||
|
</init-param>
|
||||||
|
<init-param>
|
||||||
|
<param-name>contextConfigLocation</param-name>
|
||||||
|
<param-value>ca.uhn.fhir.jpa.test.FhirTesterConfig</param-value>
|
||||||
|
</init-param>
|
||||||
|
<load-on-startup>2</load-on-startup>
|
||||||
|
</servlet>
|
||||||
|
|
||||||
|
<servlet-mapping>
|
||||||
|
<servlet-name>spring</servlet-name>
|
||||||
|
<url-pattern>/</url-pattern>
|
||||||
|
</servlet-mapping>
|
||||||
|
|
||||||
|
<jsp-config>
|
||||||
|
<jsp-property-group>
|
||||||
|
<url-pattern>*.jsp</url-pattern>
|
||||||
|
<trim-directive-whitespaces>true</trim-directive-whitespaces>
|
||||||
|
</jsp-property-group>
|
||||||
|
</jsp-config>
|
||||||
|
|
||||||
|
</web-app>
|
Loading…
Reference in New Issue