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) {
|
||||
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.add(theValidator);
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<tr th:each="issue : ${resource.issue}">
|
||||
<td th:text="${issue.severityElement.value}" style="font-weight: bold;"></td>
|
||||
<td th:text="${issue.location}"></td>
|
||||
<td><pre th:text="${issue.diagnostics}"/></td>
|
||||
<td th:text="${issue.diagnostics}"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
@ -21,6 +21,7 @@ package ca.uhn.hapi.fhir.docs;
|
|||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
||||
import ca.uhn.fhir.to.FhirTesterMvcConfig;
|
||||
import ca.uhn.fhir.to.TesterConfig;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
@ -60,14 +61,20 @@ public class FhirTesterConfig {
|
|||
retVal
|
||||
.addServer()
|
||||
.withId("home")
|
||||
.withFhirVersion(FhirVersionEnum.DSTU2)
|
||||
.withFhirVersion(FhirVersionEnum.R4)
|
||||
.withBaseUrl("${serverBase}/fhir")
|
||||
.withName("Local Tester")
|
||||
// Add a $diff button on search result rows where version > 1
|
||||
.withSearchResultRowOperation("$diff", id -> id.isVersionIdPartValidLong() && id.getVersionIdPartAsLong() > 1)
|
||||
|
||||
.addServer()
|
||||
.withId("hapi")
|
||||
.withFhirVersion(FhirVersionEnum.DSTU2)
|
||||
.withBaseUrl("http://fhirtest.uhn.ca/baseDstu2")
|
||||
.withName("Public HAPI Test Server");
|
||||
.withFhirVersion(FhirVersionEnum.R4)
|
||||
.withBaseUrl("http://hapi.fhir.org/baseR4")
|
||||
.withName("Public HAPI Test Server")
|
||||
// Disable the read and update buttons on search result rows for this server
|
||||
.withSearchResultRowInteraction(RestOperationTypeEnum.READ, id -> false)
|
||||
.withSearchResultRowInteraction(RestOperationTypeEnum.UPDATE, id -> false);
|
||||
|
||||
/*
|
||||
* Use the method below to supply a client "factory" which can be used
|
||||
|
|
|
@ -23,8 +23,10 @@ package ca.uhn.hapi.fhir.docs;
|
|||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.model.api.Include;
|
||||
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
|
||||
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
|
||||
import ca.uhn.fhir.model.api.annotation.Description;
|
||||
import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum;
|
||||
import ca.uhn.fhir.parser.DataFormatException;
|
||||
import ca.uhn.fhir.rest.annotation.Count;
|
||||
import ca.uhn.fhir.rest.annotation.*;
|
||||
|
@ -392,17 +394,33 @@ public List<Patient> getPatientHistory(
|
|||
List<Patient> retVal = new ArrayList<Patient>();
|
||||
|
||||
Patient patient = new Patient();
|
||||
patient.addName().setFamily("Smith");
|
||||
|
||||
// Set the ID and version
|
||||
patient.setId(theId.withVersion("1"));
|
||||
|
||||
// ...populate the rest...
|
||||
if (isDeleted(patient)) {
|
||||
|
||||
// If the resource is deleted, it just needs to have an ID and some metadata
|
||||
ResourceMetadataKeyEnum.DELETED_AT.put(patient, InstantType.withCurrentTime());
|
||||
ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(patient, BundleEntryTransactionMethodEnum.DELETE);
|
||||
|
||||
} else {
|
||||
|
||||
// If the resource is not deleted, it should have normal resource content
|
||||
patient.addName().setFamily("Smith"); // ..populate the rest
|
||||
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
//END SNIPPET: history
|
||||
|
||||
|
||||
private boolean isDeleted(Patient thePatient) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
//START SNIPPET: vread
|
||||
@Read(version=true)
|
||||
public Patient readOrVread(@IdParam IdType theId) {
|
||||
|
|
|
@ -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);
|
||||
assertEquals(200, response.getStatusLine().getStatusCode());
|
||||
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,
|
||||
stringContainsInOrder("<issue>", "<severity value=\"information\"/>", "<code value=\"informational\"/>", "<diagnostics value=\"No issues detected during validation\"/>",
|
||||
"</issue>"));
|
||||
|
@ -4776,7 +4776,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
|
|||
ourLog.info(resp);
|
||||
assertEquals(200, response.getStatusLine().getStatusCode());
|
||||
assertThat(resp, not(containsString("Resource has no id")));
|
||||
assertThat(resp, containsString("<pre>No issues detected during validation</pre>"));
|
||||
assertThat(resp, containsString("<td>No issues detected during validation</td>"));
|
||||
assertThat(resp,
|
||||
stringContainsInOrder("<issue>", "<severity value=\"information\"/>", "<code value=\"informational\"/>", "<diagnostics value=\"No issues detected during validation\"/>",
|
||||
"</issue>"));
|
||||
|
|
|
@ -7011,7 +7011,7 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
|
|||
ourLog.info(resp);
|
||||
assertEquals(200, response.getStatusLine().getStatusCode());
|
||||
assertThat(resp, not(containsString("Resource has no id")));
|
||||
assertThat(resp, containsString("<pre>No issues detected during validation</pre>"));
|
||||
assertThat(resp, containsString("<td>No issues detected during validation</td>"));
|
||||
assertThat(resp,
|
||||
stringContainsInOrder("<issue>", "<severity value=\"information\"/>", "<code value=\"informational\"/>", "<diagnostics value=\"No issues detected during validation\"/>",
|
||||
"</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.rest.server.interceptor.IServerInterceptor;
|
||||
import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor;
|
||||
import ca.uhn.fhirtest.ScheduledSubscriptionDeleter;
|
||||
import ca.uhn.fhirtest.interceptor.AnalyticsInterceptor;
|
||||
import ca.uhn.fhirtest.joke.HolyFooCowInterceptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
@ -99,4 +100,9 @@ public class CommonConfig {
|
|||
return "true".equalsIgnoreCase(System.getProperty("testmode.local"));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ScheduledSubscriptionDeleter scheduledSubscriptionDeleter() {
|
||||
return new ScheduledSubscriptionDeleter();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,25 +1,24 @@
|
|||
package ca.uhn.fhirtest.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowire;
|
||||
import org.springframework.context.annotation.*;
|
||||
|
||||
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||
import ca.uhn.fhir.to.FhirTesterMvcConfig;
|
||||
import ca.uhn.fhir.to.TesterConfig;
|
||||
import ca.uhn.fhirtest.mvc.SubscriptionPlaygroundController;
|
||||
import org.springframework.beans.factory.annotation.Autowire;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
import static ca.uhn.fhir.rest.api.Constants.EXTOP_VALIDATE;
|
||||
|
||||
//@formatter:off
|
||||
/**
|
||||
* This spring config file configures the web testing module. It serves two
|
||||
* purposes:
|
||||
* 1. It imports FhirTesterMvcConfig, which is the spring config for the
|
||||
* tester itself
|
||||
* tester itself
|
||||
* 2. It tells the tester which server(s) to talk to, via the testerConfig()
|
||||
* method below
|
||||
* method below
|
||||
*/
|
||||
//@Configuration
|
||||
//@Import(FhirTesterMvcConfig.class)
|
||||
//@ComponentScan(basePackages = "ca.uhn.fhirtest.mvc")
|
||||
@Configuration
|
||||
@Import(FhirTesterMvcConfig.class)
|
||||
public class FhirTesterConfig {
|
||||
|
@ -30,7 +29,7 @@ public class FhirTesterConfig {
|
|||
* server, as well as one public server. If you are creating a project to
|
||||
* deploy somewhere else, you might choose to only put your own server's
|
||||
* address here.
|
||||
*
|
||||
* <p>
|
||||
* Note the use of the ${serverBase} variable below. This will be replaced with
|
||||
* the base URL as reported by the server itself. Often for a simple Tomcat
|
||||
* (or other container) installation, this will end up being something
|
||||
|
@ -43,70 +42,81 @@ public class FhirTesterConfig {
|
|||
TesterConfig retVal = new TesterConfig();
|
||||
retVal
|
||||
.addServer()
|
||||
.withId("home_r4")
|
||||
.withFhirVersion(FhirVersionEnum.R4)
|
||||
.withBaseUrl("http://hapi.fhir.org/baseR4")
|
||||
.withName("HAPI Test Server (R4 FHIR)")
|
||||
.withId("home_r4")
|
||||
.withFhirVersion(FhirVersionEnum.R4)
|
||||
.withBaseUrl("http://hapi.fhir.org/baseR4")
|
||||
.withName("HAPI Test Server (R4 FHIR)")
|
||||
.withSearchResultRowOperation(EXTOP_VALIDATE, id -> true)
|
||||
.withSearchResultRowOperation("$diff", id -> id.isVersionIdPartValidLong() && id.getVersionIdPartAsLong() > 1)
|
||||
.withSearchResultRowOperation("$everything", id -> "Patient".equals(id.getResourceType()))
|
||||
|
||||
.addServer()
|
||||
.withId("home_r4b")
|
||||
.withFhirVersion(FhirVersionEnum.R4B)
|
||||
.withBaseUrl("http://hapi.fhir.org/baseR4B")
|
||||
.withName("HAPI Test Server (R4B FHIR)")
|
||||
.withId("home_r4b")
|
||||
.withFhirVersion(FhirVersionEnum.R4B)
|
||||
.withBaseUrl("http://hapi.fhir.org/baseR4B")
|
||||
.withName("HAPI Test Server (R4B FHIR)")
|
||||
.withSearchResultRowOperation(EXTOP_VALIDATE, id -> true)
|
||||
.withSearchResultRowOperation("$diff", id -> id.isVersionIdPartValidLong() && id.getVersionIdPartAsLong() > 1)
|
||||
.withSearchResultRowOperation("$everything", id -> "Patient".equals(id.getResourceType()))
|
||||
|
||||
.addServer()
|
||||
.withId("home_21")
|
||||
.withFhirVersion(FhirVersionEnum.DSTU3)
|
||||
.withBaseUrl("http://hapi.fhir.org/baseDstu3")
|
||||
.withName("HAPI Test Server (STU3 FHIR)")
|
||||
.withId("home_21")
|
||||
.withFhirVersion(FhirVersionEnum.DSTU3)
|
||||
.withBaseUrl("http://hapi.fhir.org/baseDstu3")
|
||||
.withName("HAPI Test Server (STU3 FHIR)")
|
||||
.withSearchResultRowOperation(EXTOP_VALIDATE, id -> true)
|
||||
.withSearchResultRowOperation("$diff", id -> id.isVersionIdPartValidLong() && id.getVersionIdPartAsLong() > 1)
|
||||
.withSearchResultRowOperation("$everything", id -> "Patient".equals(id.getResourceType()))
|
||||
|
||||
.addServer()
|
||||
.withId("hapi_dev")
|
||||
.withFhirVersion(FhirVersionEnum.DSTU2)
|
||||
.withBaseUrl("http://hapi.fhir.org/baseDstu2")
|
||||
.withName("HAPI Test Server (DSTU2 FHIR)")
|
||||
.withId("hapi_dev")
|
||||
.withFhirVersion(FhirVersionEnum.DSTU2)
|
||||
.withBaseUrl("http://hapi.fhir.org/baseDstu2")
|
||||
.withName("HAPI Test Server (DSTU2 FHIR)")
|
||||
.withSearchResultRowOperation(EXTOP_VALIDATE, id -> true)
|
||||
.withSearchResultRowOperation("$everything", id -> "Patient".equals(id.getResourceType()))
|
||||
|
||||
.addServer()
|
||||
.withId("home_r5")
|
||||
.withFhirVersion(FhirVersionEnum.R5)
|
||||
.withBaseUrl("http://hapi.fhir.org/baseR5")
|
||||
.withName("HAPI Test Server (R5 FHIR)")
|
||||
// .addServer()
|
||||
// .withId("tdl_d2")
|
||||
// .withFhirVersion(FhirVersionEnum.DSTU2)
|
||||
// .withBaseUrl("http://hapi.fhir.org/testDataLibraryDstu2")
|
||||
// .withName("Test Data Library (DSTU2 FHIR)")
|
||||
// .allowsApiKey()
|
||||
// .addServer()
|
||||
// .withId("tdl_d3")
|
||||
// .withFhirVersion(FhirVersionEnum.DSTU3)
|
||||
// .withBaseUrl("http://hapi.fhir.org/testDataLibraryStu3")
|
||||
// .withName("Test Data Library (DSTU3 FHIR)")
|
||||
// .allowsApiKey()
|
||||
.withId("home_r5")
|
||||
.withFhirVersion(FhirVersionEnum.R5)
|
||||
.withBaseUrl("http://hapi.fhir.org/baseR5")
|
||||
.withName("HAPI Test Server (R5 FHIR)")
|
||||
.withSearchResultRowOperation(EXTOP_VALIDATE, id -> true)
|
||||
.withSearchResultRowOperation("$diff", id -> id.isVersionIdPartValidLong() && id.getVersionIdPartAsLong() > 1)
|
||||
.withSearchResultRowOperation("$everything", id -> "Patient".equals(id.getResourceType()))
|
||||
|
||||
// Non-HAPI servers follow
|
||||
|
||||
.addServer()
|
||||
.withId("hi4")
|
||||
.withFhirVersion(FhirVersionEnum.DSTU3)
|
||||
.withBaseUrl("http://test.fhir.org/r4")
|
||||
.withName("Health Intersections (R4 FHIR)")
|
||||
.withId("hi4")
|
||||
.withFhirVersion(FhirVersionEnum.DSTU3)
|
||||
.withBaseUrl("http://test.fhir.org/r4")
|
||||
.withName("Health Intersections (R4 FHIR)")
|
||||
|
||||
.addServer()
|
||||
.withId("hi3")
|
||||
.withFhirVersion(FhirVersionEnum.DSTU3)
|
||||
.withBaseUrl("http://test.fhir.org/r3")
|
||||
.withName("Health Intersections (STU3 FHIR)")
|
||||
.withId("hi3")
|
||||
.withFhirVersion(FhirVersionEnum.DSTU3)
|
||||
.withBaseUrl("http://test.fhir.org/r3")
|
||||
.withName("Health Intersections (STU3 FHIR)")
|
||||
|
||||
.addServer()
|
||||
.withId("hi2")
|
||||
.withFhirVersion(FhirVersionEnum.DSTU2)
|
||||
.withBaseUrl("http://test.fhir.org/r2")
|
||||
.withName("Health Intersections (DSTU2 FHIR)")
|
||||
.withId("hi2")
|
||||
.withFhirVersion(FhirVersionEnum.DSTU2)
|
||||
.withBaseUrl("http://test.fhir.org/r2")
|
||||
.withName("Health Intersections (DSTU2 FHIR)")
|
||||
|
||||
.addServer()
|
||||
.withId("spark2")
|
||||
.withFhirVersion(FhirVersionEnum.DSTU3)
|
||||
.withBaseUrl("http://vonk.fire.ly/")
|
||||
.withName("Vonk - Firely (STU3 FHIR)");
|
||||
.withId("spark2")
|
||||
.withFhirVersion(FhirVersionEnum.DSTU3)
|
||||
.withBaseUrl("http://vonk.fire.ly/")
|
||||
.withName("Vonk - Firely (STU3 FHIR)");
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@Bean(autowire=Autowire.BY_TYPE)
|
||||
@Bean(autowire = Autowire.BY_TYPE)
|
||||
public SubscriptionPlaygroundController subscriptionPlaygroundController() {
|
||||
return new SubscriptionPlaygroundController();
|
||||
}
|
||||
|
||||
}
|
||||
//@formatter:on
|
||||
|
|
|
@ -191,7 +191,7 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
|
|||
}
|
||||
if (isBlank(nextResource.getIdElement().getVersionIdPart()) && nextResource instanceof IResource) {
|
||||
//TODO: Use of a deprecated method should be resolved.
|
||||
IdDt versionId = ResourceMetadataKeyEnum.VERSION_ID.get((IResource) nextResource);
|
||||
IdDt versionId = ResourceMetadataKeyEnum.VERSION_ID.get(nextResource);
|
||||
if (versionId == null || versionId.isEmpty()) {
|
||||
throw new InternalErrorException(Msg.code(411) + "Server provided resource at index " + index + " with no Version ID set (using IResource#setId(IdDt))");
|
||||
}
|
||||
|
|
|
@ -20,17 +20,16 @@ package ca.uhn.fhir.rest.server.provider;
|
|||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
|
||||
import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.interceptor.api.HookParams;
|
||||
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
|
||||
import ca.uhn.fhir.model.api.IResource;
|
||||
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
|
||||
import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum;
|
||||
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
|
||||
import ca.uhn.fhir.rest.annotation.Create;
|
||||
import ca.uhn.fhir.rest.annotation.Delete;
|
||||
|
@ -68,6 +67,7 @@ import org.slf4j.LoggerFactory;
|
|||
import javax.annotation.Nonnull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
@ -103,10 +103,10 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
|||
protected LinkedList<T> myTypeHistory = new LinkedList<>();
|
||||
protected AtomicLong mySearchCount = new AtomicLong(0);
|
||||
private long myNextId;
|
||||
private AtomicLong myDeleteCount = new AtomicLong(0);
|
||||
private AtomicLong myUpdateCount = new AtomicLong(0);
|
||||
private AtomicLong myCreateCount = new AtomicLong(0);
|
||||
private AtomicLong myReadCount = new AtomicLong(0);
|
||||
private final AtomicLong myDeleteCount = new AtomicLong(0);
|
||||
private final AtomicLong myUpdateCount = new AtomicLong(0);
|
||||
private final AtomicLong myCreateCount = new AtomicLong(0);
|
||||
private final AtomicLong myReadCount = new AtomicLong(0);
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
@ -134,7 +134,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
|||
/**
|
||||
* Clear the counts used by {@link #getCountRead()} and other count methods
|
||||
*/
|
||||
public synchronized void clearCounts() {
|
||||
public synchronized void clearCounts() {
|
||||
myReadCount.set(0L);
|
||||
myUpdateCount.set(0L);
|
||||
myCreateCount.set(0L);
|
||||
|
@ -163,12 +163,12 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
|||
|
||||
assert !myIdToVersionToResourceMap.containsKey(idPartAsString);
|
||||
|
||||
IIdType id = store(theResource, idPartAsString, versionIdPart, theRequestDetails, theTransactionDetails);
|
||||
theResource.setId(id);
|
||||
store(theResource, idPartAsString, versionIdPart, theRequestDetails, theTransactionDetails, false);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@Delete
|
||||
public synchronized MethodOutcome delete(@IdParam IIdType theId, RequestDetails theRequestDetails) {
|
||||
public synchronized MethodOutcome delete(@IdParam IIdType theId, RequestDetails theRequestDetails) {
|
||||
TransactionDetails transactionDetails = new TransactionDetails();
|
||||
|
||||
TreeMap<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);
|
||||
}
|
||||
|
||||
|
||||
T deletedInstance = (T) myFhirContext.getResourceDefinition(myResourceType).newInstance();
|
||||
long nextVersion = versions.lastEntry().getKey() + 1L;
|
||||
IIdType id = store(null, theId.getIdPart(), nextVersion, theRequestDetails, transactionDetails);
|
||||
IIdType id = store(deletedInstance, theId.getIdPart(), nextVersion, theRequestDetails, transactionDetails, true);
|
||||
|
||||
myDeleteCount.incrementAndGet();
|
||||
|
||||
|
@ -190,7 +190,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
|||
* This method returns a simple operation count. This is mostly
|
||||
* useful for testing purposes.
|
||||
*/
|
||||
public synchronized long getCountCreate() {
|
||||
public synchronized long getCountCreate() {
|
||||
return myCreateCount.get();
|
||||
}
|
||||
|
||||
|
@ -198,7 +198,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
|||
* This method returns a simple operation count. This is mostly
|
||||
* useful for testing purposes.
|
||||
*/
|
||||
public synchronized long getCountDelete() {
|
||||
public synchronized long getCountDelete() {
|
||||
return myDeleteCount.get();
|
||||
}
|
||||
|
||||
|
@ -206,7 +206,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
|||
* This method returns a simple operation count. This is mostly
|
||||
* useful for testing purposes.
|
||||
*/
|
||||
public synchronized long getCountRead() {
|
||||
public synchronized long getCountRead() {
|
||||
return myReadCount.get();
|
||||
}
|
||||
|
||||
|
@ -214,7 +214,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
|||
* This method returns a simple operation count. This is mostly
|
||||
* useful for testing purposes.
|
||||
*/
|
||||
public synchronized long getCountSearch() {
|
||||
public synchronized long getCountSearch() {
|
||||
return mySearchCount.get();
|
||||
}
|
||||
|
||||
|
@ -222,7 +222,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
|||
* This method returns a simple operation count. This is mostly
|
||||
* useful for testing purposes.
|
||||
*/
|
||||
public synchronized long getCountUpdate() {
|
||||
public synchronized long getCountUpdate() {
|
||||
return myUpdateCount.get();
|
||||
}
|
||||
|
||||
|
@ -237,7 +237,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
|||
}
|
||||
|
||||
@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());
|
||||
if (retVal == null) {
|
||||
throw new ResourceNotFoundException(Msg.code(1980) + theId);
|
||||
|
@ -252,7 +252,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
|||
}
|
||||
|
||||
@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());
|
||||
if (versions == null || versions.isEmpty()) {
|
||||
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);
|
||||
} else {
|
||||
T resource = versions.get(versionId);
|
||||
if (resource == null) {
|
||||
if (resource == null || ResourceMetadataKeyEnum.DELETED_AT.get(resource) != null) {
|
||||
throw new ResourceGoneException(Msg.code(1983) + theId);
|
||||
}
|
||||
retVal = resource;
|
||||
|
@ -285,21 +285,26 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
|||
}
|
||||
|
||||
@Search
|
||||
public synchronized List<IBaseResource> searchAll(RequestDetails theRequestDetails) {
|
||||
public synchronized List<IBaseResource> searchAll(RequestDetails theRequestDetails) {
|
||||
mySearchCount.incrementAndGet();
|
||||
List<T> retVal = getAllResources();
|
||||
return fireInterceptorsAndFilterAsNeeded(retVal, theRequestDetails);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
protected synchronized List<T> getAllResources() {
|
||||
protected synchronized List<T> getAllResources() {
|
||||
List<T> retVal = new ArrayList<>();
|
||||
|
||||
for (TreeMap<Long, T> next : myIdToVersionToResourceMap.values()) {
|
||||
if (next.isEmpty() == false) {
|
||||
T nextResource = next.lastEntry().getValue();
|
||||
if (nextResource != null) {
|
||||
retVal.add(nextResource);
|
||||
if (ResourceMetadataKeyEnum.DELETED_AT.get(nextResource) == null) {
|
||||
// Clone the resource for search results so that the
|
||||
// stored metadata doesn't appear in the results
|
||||
T nextResourceClone = myFhirContext.newTerser().clone(nextResource);
|
||||
retVal.add(nextResourceClone);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -308,7 +313,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
|||
}
|
||||
|
||||
@Search
|
||||
public synchronized List<IBaseResource> searchById(
|
||||
public synchronized List<IBaseResource> searchById(
|
||||
@RequiredParam(name = "_id") TokenAndListParam theIds, RequestDetails theRequestDetails) {
|
||||
|
||||
List<T> retVal = new ArrayList<>();
|
||||
|
@ -345,12 +350,25 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
|||
return fireInterceptorsAndFilterAsNeeded(retVal, theRequestDetails);
|
||||
}
|
||||
|
||||
private IIdType store(@ResourceParam T theResource, String theIdPart, Long theVersionIdPart, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails) {
|
||||
@SuppressWarnings({"unchecked", "DataFlowIssue"})
|
||||
private IIdType store(@Nonnull T theResource, String theIdPart, Long theVersionIdPart, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails, boolean theDeleted) {
|
||||
IIdType id = myFhirContext.getVersion().newIdType();
|
||||
String versionIdPart = Long.toString(theVersionIdPart);
|
||||
id.setParts(null, myResourceName, theIdPart, versionIdPart);
|
||||
if (theResource != null) {
|
||||
theResource.setId(id);
|
||||
theResource.setId(id);
|
||||
|
||||
if (theDeleted) {
|
||||
IPrimitiveType<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
|
||||
* that was assigned by this provider
|
||||
*/
|
||||
if (theResource != null) {
|
||||
if (myFhirContext.getVersion().getVersion() == FhirVersionEnum.DSTU2) {
|
||||
ResourceMetadataKeyEnum.VERSION.put((IResource) theResource, versionIdPart);
|
||||
} else {
|
||||
BaseRuntimeChildDefinition metaChild = myFhirContext.getResourceDefinition(myResourceType).getChildByName("meta");
|
||||
List<IBase> metaValues = metaChild.getAccessor().getValues(theResource);
|
||||
if (metaValues.size() > 0) {
|
||||
IBase meta = metaValues.get(0);
|
||||
BaseRuntimeElementCompositeDefinition<?> metaDef = (BaseRuntimeElementCompositeDefinition<?>) myFhirContext.getElementDefinition(meta.getClass());
|
||||
BaseRuntimeChildDefinition versionIdDef = metaDef.getChildByName("versionId");
|
||||
List<IBase> versionIdValues = versionIdDef.getAccessor().getValues(meta);
|
||||
if (versionIdValues.size() > 0) {
|
||||
IPrimitiveType<?> versionId = (IPrimitiveType<?>) versionIdValues.get(0);
|
||||
versionId.setValueAsString(versionIdPart);
|
||||
}
|
||||
}
|
||||
if (myFhirContext.getVersion().getVersion() == FhirVersionEnum.DSTU2) {
|
||||
ResourceMetadataKeyEnum.VERSION.put(theResource, versionIdPart);
|
||||
} else {
|
||||
BaseRuntimeChildDefinition metaChild = myFhirContext.getResourceDefinition(myResourceType).getChildByName("meta");
|
||||
List<IBase> metaValues = metaChild.getAccessor().getValues(theResource);
|
||||
if (metaValues.size() > 0) {
|
||||
theResource.getMeta().setVersionId(versionIdPart);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -383,52 +392,70 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
|||
TreeMap<Long, T> versionToResource = getVersionToResource(theIdPart);
|
||||
versionToResource.put(theVersionIdPart, theResource);
|
||||
|
||||
if (theRequestDetails != null) {
|
||||
if (theRequestDetails != null && theRequestDetails.getInterceptorBroadcaster() != null) {
|
||||
IInterceptorBroadcaster interceptorBroadcaster = theRequestDetails.getInterceptorBroadcaster();
|
||||
|
||||
if (theResource != null) {
|
||||
if (!myIdToHistory.containsKey(theIdPart)) {
|
||||
if (theDeleted) {
|
||||
|
||||
// Interceptor call: STORAGE_PRESTORAGE_RESOURCE_CREATED
|
||||
HookParams preStorageParams = new HookParams()
|
||||
.add(RequestDetails.class, theRequestDetails)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
|
||||
.add(IBaseResource.class, theResource)
|
||||
.add(RequestPartitionId.class, null) // we should add this if we want - but this is test usage
|
||||
.add(TransactionDetails.class, theTransactionDetails);
|
||||
interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, preStorageParams);
|
||||
// Interceptor call: STORAGE_PRESTORAGE_RESOURCE_DELETED
|
||||
HookParams preStorageParams = new HookParams()
|
||||
.add(RequestDetails.class, theRequestDetails)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
|
||||
.add(IBaseResource.class, myIdToHistory.get(theIdPart).getFirst())
|
||||
.add(TransactionDetails.class, theTransactionDetails);
|
||||
interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, preStorageParams);
|
||||
|
||||
// Interceptor call: STORAGE_PRECOMMIT_RESOURCE_CREATED
|
||||
HookParams preCommitParams = new HookParams()
|
||||
.add(RequestDetails.class, theRequestDetails)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
|
||||
.add(IBaseResource.class, theResource)
|
||||
.add(TransactionDetails.class, theTransactionDetails)
|
||||
.add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED));
|
||||
interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED, preCommitParams);
|
||||
// Interceptor call: STORAGE_PRECOMMIT_RESOURCE_DELETED
|
||||
HookParams preCommitParams = new HookParams()
|
||||
.add(RequestDetails.class, theRequestDetails)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
|
||||
.add(IBaseResource.class, myIdToHistory.get(theIdPart).getFirst())
|
||||
.add(TransactionDetails.class, theTransactionDetails)
|
||||
.add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED));
|
||||
interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, preCommitParams);
|
||||
|
||||
} else {
|
||||
|
||||
// Interceptor call: STORAGE_PRESTORAGE_RESOURCE_UPDATED
|
||||
HookParams preStorageParams = new HookParams()
|
||||
.add(RequestDetails.class, theRequestDetails)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
|
||||
.add(IBaseResource.class, myIdToHistory.get(theIdPart).getFirst())
|
||||
.add(IBaseResource.class, theResource)
|
||||
.add(TransactionDetails.class, theTransactionDetails);
|
||||
interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, preStorageParams);
|
||||
} else if (!myIdToHistory.containsKey(theIdPart)) {
|
||||
|
||||
// Interceptor call: STORAGE_PRECOMMIT_RESOURCE_UPDATED
|
||||
HookParams preCommitParams = new HookParams()
|
||||
.add(RequestDetails.class, theRequestDetails)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
|
||||
.add(IBaseResource.class, myIdToHistory.get(theIdPart).getFirst())
|
||||
.add(IBaseResource.class, theResource)
|
||||
.add(TransactionDetails.class, theTransactionDetails)
|
||||
.add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED));
|
||||
interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, preCommitParams);
|
||||
// Interceptor call: STORAGE_PRESTORAGE_RESOURCE_CREATED
|
||||
HookParams preStorageParams = new HookParams()
|
||||
.add(RequestDetails.class, theRequestDetails)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
|
||||
.add(IBaseResource.class, theResource)
|
||||
.add(RequestPartitionId.class, null) // we should add this if we want - but this is test usage
|
||||
.add(TransactionDetails.class, theTransactionDetails);
|
||||
interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, preStorageParams);
|
||||
|
||||
// Interceptor call: STORAGE_PRECOMMIT_RESOURCE_CREATED
|
||||
HookParams preCommitParams = new HookParams()
|
||||
.add(RequestDetails.class, theRequestDetails)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
|
||||
.add(IBaseResource.class, theResource)
|
||||
.add(TransactionDetails.class, theTransactionDetails)
|
||||
.add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED));
|
||||
interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED, preCommitParams);
|
||||
|
||||
} else {
|
||||
|
||||
// Interceptor call: STORAGE_PRESTORAGE_RESOURCE_UPDATED
|
||||
HookParams preStorageParams = new HookParams()
|
||||
.add(RequestDetails.class, theRequestDetails)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
|
||||
.add(IBaseResource.class, myIdToHistory.get(theIdPart).getFirst())
|
||||
.add(IBaseResource.class, theResource)
|
||||
.add(TransactionDetails.class, theTransactionDetails);
|
||||
interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, preStorageParams);
|
||||
|
||||
// Interceptor call: STORAGE_PRECOMMIT_RESOURCE_UPDATED
|
||||
HookParams preCommitParams = new HookParams()
|
||||
.add(RequestDetails.class, theRequestDetails)
|
||||
.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
|
||||
.add(IBaseResource.class, myIdToHistory.get(theIdPart).getFirst())
|
||||
.add(IBaseResource.class, theResource)
|
||||
.add(TransactionDetails.class, theTransactionDetails)
|
||||
.add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED));
|
||||
interceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, preCommitParams);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -447,7 +474,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
|||
* @param theConditional This is provided only so that subclasses can implement if they want
|
||||
*/
|
||||
@Update
|
||||
public synchronized MethodOutcome update(
|
||||
public synchronized MethodOutcome update(
|
||||
@ResourceParam T theResource,
|
||||
@ConditionalUrlParam String theConditional,
|
||||
RequestDetails theRequestDetails) {
|
||||
|
@ -478,7 +505,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
|||
created = false;
|
||||
}
|
||||
|
||||
IIdType id = store(theResource, idPartAsString, versionIdPart, theRequestDetails, theTransactionDetails);
|
||||
IIdType id = store(theResource, idPartAsString, versionIdPart, theRequestDetails, theTransactionDetails, false);
|
||||
theResource.setId(id);
|
||||
return created;
|
||||
}
|
||||
|
@ -495,7 +522,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
|||
* @param theResource The resource to store. If the resource has an ID, that ID is updated.
|
||||
* @return Return the ID assigned to the stored resource
|
||||
*/
|
||||
public synchronized IIdType store(T theResource) {
|
||||
public synchronized IIdType store(T theResource) {
|
||||
if (theResource.getIdElement().hasIdPart()) {
|
||||
updateInternal(theResource, null, new TransactionDetails());
|
||||
} else {
|
||||
|
@ -509,7 +536,7 @@ public class HashMapResourceProvider<T extends IBaseResource> implements IResour
|
|||
*
|
||||
* @since 4.1.0
|
||||
*/
|
||||
public synchronized List<T> getStoredResources() {
|
||||
public synchronized List<T> getStoredResources() {
|
||||
List<T> retVal = new ArrayList<>();
|
||||
for (TreeMap<Long, T> next : myIdToVersionToResourceMap.values()) {
|
||||
retVal.add(next.lastEntry().getValue());
|
||||
|
|
|
@ -103,7 +103,7 @@ public class DefaultThymeleafNarrativeGeneratorDstu2Test {
|
|||
|
||||
ourLog.info(output);
|
||||
|
||||
assertThat(output, containsString("<td><pre>YThis is a warning</pre></td>"));
|
||||
assertThat(output, containsString("<td>YThis is a warning</td>"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -163,7 +163,7 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test {
|
|||
|
||||
ourLog.info(output);
|
||||
|
||||
assertThat(output, containsString("<td><pre>YThis is a warning</pre></td>"));
|
||||
assertThat(output, containsString("<td>YThis is a warning</td>"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -111,7 +111,7 @@ public class DefaultThymeleafNarrativeGeneratorR4Test {
|
|||
|
||||
ourLog.info(output);
|
||||
|
||||
assertThat(output, containsString("<td><pre>YThis is a warning</pre></td>"));
|
||||
assertThat(output, containsString("<td>YThis is a warning</td>"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -3,35 +3,31 @@ package ca.uhn.fhir.rest.server.provider;
|
|||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor;
|
||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||
import ca.uhn.fhir.rest.client.api.IGenericClient;
|
||||
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
|
||||
import ca.uhn.fhir.rest.gclient.IDeleteTyped;
|
||||
import ca.uhn.fhir.rest.server.IResourceProvider;
|
||||
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||
import ca.uhn.fhir.test.utilities.JettyUtil;
|
||||
import ca.uhn.fhir.rest.server.interceptor.ResponseValidatingInterceptor;
|
||||
import ca.uhn.fhir.test.utilities.server.HashMapResourceProviderExtension;
|
||||
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
|
||||
import ca.uhn.fhir.util.TestUtil;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import ca.uhn.fhir.validation.FhirValidator;
|
||||
import ca.uhn.fhir.validation.ResultSeverityEnum;
|
||||
import org.hl7.fhir.instance.model.api.IAnyResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.hl7.fhir.r4.model.Bundle;
|
||||
import org.hl7.fhir.r4.model.Observation;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
@ -40,6 +36,8 @@ import static org.hamcrest.Matchers.contains;
|
|||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.matchesPattern;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
|
@ -48,24 +46,22 @@ import static org.mockito.Mockito.verify;
|
|||
@ExtendWith(MockitoExtension.class)
|
||||
public class HashMapResourceProviderTest {
|
||||
|
||||
private static final FhirContext ourCtx = FhirContext.forR4Cached();
|
||||
@RegisterExtension
|
||||
@Order(0)
|
||||
private static final RestfulServerExtension ourRestServer = new RestfulServerExtension(ourCtx);
|
||||
@RegisterExtension
|
||||
@Order(1)
|
||||
private static final HashMapResourceProviderExtension<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 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
|
||||
private IAnonymousInterceptor myAnonymousInterceptor;
|
||||
|
||||
@BeforeEach
|
||||
public void before() {
|
||||
ourRestServer.clearData();
|
||||
myPatientResourceProvider.clearCounts();
|
||||
myObservationResourceProvider.clearCounts();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateAndRead() {
|
||||
ourRestServer.getInterceptorService().registerAnonymousInterceptor(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, myAnonymousInterceptor);
|
||||
|
@ -74,7 +70,7 @@ public class HashMapResourceProviderTest {
|
|||
// Create
|
||||
Patient p = new Patient();
|
||||
p.setActive(true);
|
||||
IIdType id = ourClient.create().resource(p).execute().getId();
|
||||
IIdType id = ourRestServer.getFhirClient().create().resource(p).execute().getId();
|
||||
assertThat(id.getIdPart(), matchesPattern("[0-9]+"));
|
||||
assertEquals("1", id.getVersionIdPart());
|
||||
|
||||
|
@ -82,8 +78,8 @@ public class HashMapResourceProviderTest {
|
|||
verify(myAnonymousInterceptor, Mockito.times(1)).invoke(eq(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED), any());
|
||||
|
||||
// Read
|
||||
p = (Patient) ourClient.read().resource("Patient").withId(id).execute();
|
||||
assertEquals(true, p.getActive());
|
||||
p = (Patient) ourRestServer.getFhirClient().read().resource("Patient").withId(id).execute();
|
||||
assertTrue(p.getActive());
|
||||
|
||||
assertEquals(1, myPatientResourceProvider.getCountRead());
|
||||
}
|
||||
|
@ -94,13 +90,13 @@ public class HashMapResourceProviderTest {
|
|||
Patient p = new Patient();
|
||||
p.setId("ABC");
|
||||
p.setActive(true);
|
||||
IIdType id = ourClient.update().resource(p).execute().getId();
|
||||
IIdType id = ourRestServer.getFhirClient().update().resource(p).execute().getId();
|
||||
assertEquals("ABC", id.getIdPart());
|
||||
assertEquals("1", id.getVersionIdPart());
|
||||
|
||||
// Read
|
||||
p = (Patient) ourClient.read().resource("Patient").withId(id).execute();
|
||||
assertEquals(true, p.getActive());
|
||||
p = (Patient) ourRestServer.getFhirClient().read().resource("Patient").withId(id).execute();
|
||||
assertTrue(p.getActive());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -108,31 +104,39 @@ public class HashMapResourceProviderTest {
|
|||
// Create
|
||||
Patient p = new Patient();
|
||||
p.setActive(true);
|
||||
IIdType id = ourClient.create().resource(p).execute().getId();
|
||||
IIdType id = ourRestServer.getFhirClient().create().resource(p).execute().getId().toUnqualified();
|
||||
assertThat(id.getIdPart(), matchesPattern("[0-9]+"));
|
||||
assertEquals("1", id.getVersionIdPart());
|
||||
|
||||
assertEquals(0, myPatientResourceProvider.getCountDelete());
|
||||
|
||||
IDeleteTyped iDeleteTyped = ourClient.delete().resourceById(id.toUnqualifiedVersionless());
|
||||
ourRestServer.getFhirClient().delete().resourceById(id.toUnqualifiedVersionless()).execute();
|
||||
ourLog.info("About to execute");
|
||||
try {
|
||||
iDeleteTyped.execute();
|
||||
} catch (NullPointerException e) {
|
||||
ourLog.error("NPE", e);
|
||||
fail(e.toString());
|
||||
}
|
||||
|
||||
assertEquals(1, myPatientResourceProvider.getCountDelete());
|
||||
|
||||
// Read
|
||||
ourClient.read().resource("Patient").withId(id.withVersion("1")).execute();
|
||||
ourRestServer.getFhirClient().read().resource("Patient").withId(id.withVersion("1")).execute();
|
||||
try {
|
||||
ourClient.read().resource("Patient").withId(id.withVersion("2")).execute();
|
||||
ourRestServer.getFhirClient().read().resource("Patient").withId(id.withVersion("2")).execute();
|
||||
fail();
|
||||
} catch (ResourceGoneException e) {
|
||||
// good
|
||||
}
|
||||
|
||||
// History should include deleted entry
|
||||
Bundle history = ourRestServer.getFhirClient().history().onType(Patient.class).returnBundle(Bundle.class).execute();
|
||||
ourLog.info("History:\n{}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(history));
|
||||
assertEquals(id.withVersion("2").getValue(), history.getEntry().get(0).getRequest().getUrl());
|
||||
assertEquals("DELETE", history.getEntry().get(0).getRequest().getMethod().toCode());
|
||||
assertEquals(id.withVersion("1").getValue(), history.getEntry().get(1).getRequest().getUrl());
|
||||
assertEquals("POST", history.getEntry().get(1).getRequest().getMethod().toCode());
|
||||
|
||||
// Search should not include deleted entry
|
||||
Bundle search = ourRestServer.getFhirClient().search().forResource("Patient").returnBundle(Bundle.class).execute();
|
||||
ourLog.info("Search:\n{}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(search));
|
||||
assertEquals(0, search.getEntry().size());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -140,14 +144,14 @@ public class HashMapResourceProviderTest {
|
|||
// Create Res 1
|
||||
Patient p = new Patient();
|
||||
p.setActive(true);
|
||||
IIdType id1 = ourClient.create().resource(p).execute().getId();
|
||||
IIdType id1 = ourRestServer.getFhirClient().create().resource(p).execute().getId();
|
||||
assertThat(id1.getIdPart(), matchesPattern("[0-9]+"));
|
||||
assertEquals("1", id1.getVersionIdPart());
|
||||
|
||||
// Create Res 2
|
||||
p = new Patient();
|
||||
p.setActive(true);
|
||||
IIdType id2 = ourClient.create().resource(p).execute().getId();
|
||||
IIdType id2 = ourRestServer.getFhirClient().create().resource(p).execute().getId();
|
||||
assertThat(id2.getIdPart(), matchesPattern("[0-9]+"));
|
||||
assertEquals("1", id2.getVersionIdPart());
|
||||
|
||||
|
@ -155,11 +159,11 @@ public class HashMapResourceProviderTest {
|
|||
p = new Patient();
|
||||
p.setId(id2);
|
||||
p.setActive(false);
|
||||
id2 = ourClient.update().resource(p).execute().getId();
|
||||
id2 = ourRestServer.getFhirClient().update().resource(p).execute().getId();
|
||||
assertThat(id2.getIdPart(), matchesPattern("[0-9]+"));
|
||||
assertEquals("2", id2.getVersionIdPart());
|
||||
|
||||
Bundle history = ourClient
|
||||
Bundle history = ourRestServer.getFhirClient()
|
||||
.history()
|
||||
.onInstance(id2.toUnqualifiedVersionless())
|
||||
.andReturnBundle(Bundle.class)
|
||||
|
@ -184,14 +188,14 @@ public class HashMapResourceProviderTest {
|
|||
// Create Res 1
|
||||
Patient p = new Patient();
|
||||
p.setActive(true);
|
||||
IIdType id1 = ourClient.create().resource(p).execute().getId();
|
||||
IIdType id1 = ourRestServer.getFhirClient().create().resource(p).execute().getId();
|
||||
assertThat(id1.getIdPart(), matchesPattern("[0-9]+"));
|
||||
assertEquals("1", id1.getVersionIdPart());
|
||||
|
||||
// Create Res 2
|
||||
p = new Patient();
|
||||
p.setActive(true);
|
||||
IIdType id2 = ourClient.create().resource(p).execute().getId();
|
||||
IIdType id2 = ourRestServer.getFhirClient().create().resource(p).execute().getId();
|
||||
assertThat(id2.getIdPart(), matchesPattern("[0-9]+"));
|
||||
assertEquals("1", id2.getVersionIdPart());
|
||||
|
||||
|
@ -199,11 +203,11 @@ public class HashMapResourceProviderTest {
|
|||
p = new Patient();
|
||||
p.setId(id2);
|
||||
p.setActive(false);
|
||||
id2 = ourClient.update().resource(p).execute().getId();
|
||||
id2 = ourRestServer.getFhirClient().update().resource(p).execute().getId();
|
||||
assertThat(id2.getIdPart(), matchesPattern("[0-9]+"));
|
||||
assertEquals("2", id2.getVersionIdPart());
|
||||
|
||||
Bundle history = ourClient
|
||||
Bundle history = ourRestServer.getFhirClient()
|
||||
.history()
|
||||
.onType(Patient.class)
|
||||
.andReturnBundle(Bundle.class)
|
||||
|
@ -228,20 +232,23 @@ public class HashMapResourceProviderTest {
|
|||
for (int i = 0; i < 100; i++) {
|
||||
Patient p = new Patient();
|
||||
p.addName().setFamily("FAM" + i);
|
||||
ourClient.registerInterceptor(new LoggingInterceptor(true));
|
||||
IIdType id = ourClient.create().resource(p).execute().getId();
|
||||
ourRestServer.getFhirClient().registerInterceptor(new LoggingInterceptor(true));
|
||||
IIdType id = ourRestServer.getFhirClient().create().resource(p).execute().getId();
|
||||
assertThat(id.getIdPart(), matchesPattern("[0-9]+"));
|
||||
assertEquals("1", id.getVersionIdPart());
|
||||
}
|
||||
|
||||
// Search
|
||||
Bundle resp = ourClient
|
||||
Bundle resp = ourRestServer.getFhirClient()
|
||||
.search()
|
||||
.forResource("Patient")
|
||||
.returnBundle(Bundle.class)
|
||||
.execute();
|
||||
ourLog.info("Search:\n{}", ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(resp));
|
||||
assertEquals(100, resp.getTotal());
|
||||
assertEquals(100, resp.getEntry().size());
|
||||
assertFalse(resp.getEntry().get(0).hasRequest());
|
||||
assertFalse(resp.getEntry().get(1).hasRequest());
|
||||
|
||||
assertEquals(1, myPatientResourceProvider.getCountSearch());
|
||||
|
||||
|
@ -253,13 +260,13 @@ public class HashMapResourceProviderTest {
|
|||
for (int i = 0; i < 100; i++) {
|
||||
Patient p = new Patient();
|
||||
p.addName().setFamily("FAM" + i);
|
||||
IIdType id = ourClient.create().resource(p).execute().getId();
|
||||
IIdType id = ourRestServer.getFhirClient().create().resource(p).execute().getId();
|
||||
assertThat(id.getIdPart(), matchesPattern("[0-9]+"));
|
||||
assertEquals("1", id.getVersionIdPart());
|
||||
}
|
||||
|
||||
// Search
|
||||
Bundle resp = ourClient
|
||||
Bundle resp = ourRestServer.getFhirClient()
|
||||
.search()
|
||||
.forResource("Patient")
|
||||
.where(IAnyResource.RES_ID.exactly().codes("2", "3"))
|
||||
|
@ -270,7 +277,7 @@ public class HashMapResourceProviderTest {
|
|||
assertThat(respIds, containsInAnyOrder("Patient/2", "Patient/3"));
|
||||
|
||||
// Search
|
||||
resp = ourClient
|
||||
resp = ourRestServer.getFhirClient()
|
||||
.search()
|
||||
.forResource("Patient")
|
||||
.where(IAnyResource.RES_ID.exactly().codes("2", "3"))
|
||||
|
@ -281,7 +288,7 @@ public class HashMapResourceProviderTest {
|
|||
respIds = resp.getEntry().stream().map(t -> t.getResource().getIdElement().toUnqualifiedVersionless().getValue()).collect(Collectors.toList());
|
||||
assertThat(respIds, containsInAnyOrder("Patient/2", "Patient/3"));
|
||||
|
||||
resp = ourClient
|
||||
resp = ourRestServer.getFhirClient()
|
||||
.search()
|
||||
.forResource("Patient")
|
||||
.where(IAnyResource.RES_ID.exactly().codes("2", "3"))
|
||||
|
@ -299,7 +306,7 @@ public class HashMapResourceProviderTest {
|
|||
// Create
|
||||
Patient p = new Patient();
|
||||
p.setActive(true);
|
||||
IIdType id = ourClient.create().resource(p).execute().getId();
|
||||
IIdType id = ourRestServer.getFhirClient().create().resource(p).execute().getId();
|
||||
assertThat(id.getIdPart(), matchesPattern("[0-9]+"));
|
||||
assertEquals("1", id.getVersionIdPart());
|
||||
|
||||
|
@ -310,7 +317,7 @@ public class HashMapResourceProviderTest {
|
|||
p = new Patient();
|
||||
p.setId(id);
|
||||
p.setActive(false);
|
||||
id = ourClient.update().resource(p).execute().getId();
|
||||
id = ourRestServer.getFhirClient().update().resource(p).execute().getId();
|
||||
assertThat(id.getIdPart(), matchesPattern("[0-9]+"));
|
||||
assertEquals("2", id.getVersionIdPart());
|
||||
|
||||
|
@ -321,71 +328,21 @@ public class HashMapResourceProviderTest {
|
|||
assertEquals(1, myPatientResourceProvider.getCountUpdate());
|
||||
|
||||
// Read
|
||||
p = (Patient) ourClient.read().resource("Patient").withId(id.withVersion("1")).execute();
|
||||
assertEquals(true, p.getActive());
|
||||
p = (Patient) ourClient.read().resource("Patient").withId(id.withVersion("2")).execute();
|
||||
assertEquals(false, p.getActive());
|
||||
p = (Patient) ourRestServer.getFhirClient().read().resource("Patient").withId(id.withVersion("1")).execute();
|
||||
assertTrue(p.getActive());
|
||||
p = (Patient) ourRestServer.getFhirClient().read().resource("Patient").withId(id.withVersion("2")).execute();
|
||||
assertFalse(p.getActive());
|
||||
try {
|
||||
ourClient.read().resource("Patient").withId(id.withVersion("3")).execute();
|
||||
ourRestServer.getFhirClient().read().resource("Patient").withId(id.withVersion("3")).execute();
|
||||
fail();
|
||||
} catch (ResourceNotFoundException e) {
|
||||
// good
|
||||
}
|
||||
}
|
||||
|
||||
private static class MyRestfulServer extends RestfulServer {
|
||||
|
||||
MyRestfulServer() {
|
||||
super(ourCtx);
|
||||
}
|
||||
|
||||
void clearData() {
|
||||
for (IResourceProvider next : getResourceProviders()) {
|
||||
if (next instanceof HashMapResourceProvider) {
|
||||
((HashMapResourceProvider) next).clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialize() throws ServletException {
|
||||
super.initialize();
|
||||
|
||||
myPatientResourceProvider = new HashMapResourceProvider<>(ourCtx, Patient.class);
|
||||
myObservationResourceProvider = new HashMapResourceProvider<>(ourCtx, Observation.class);
|
||||
registerProvider(myPatientResourceProvider);
|
||||
registerProvider(myObservationResourceProvider);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void afterClassClearContext() throws Exception {
|
||||
JettyUtil.closeServer(ourListenerServer);
|
||||
TestUtil.randomizeLocaleAndTimezone();
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
public static void startListenerServer() throws Exception {
|
||||
ourRestServer = new MyRestfulServer();
|
||||
|
||||
ourListenerServer = new Server(0);
|
||||
|
||||
ServletContextHandler proxyHandler = new ServletContextHandler();
|
||||
proxyHandler.setContextPath("/");
|
||||
|
||||
ServletHolder servletHolder = new ServletHolder();
|
||||
servletHolder.setServlet(ourRestServer);
|
||||
proxyHandler.addServlet(servletHolder, "/*");
|
||||
|
||||
ourListenerServer.setHandler(proxyHandler);
|
||||
JettyUtil.startServer(ourListenerServer);
|
||||
int ourListenerPort = JettyUtil.getPortForStartedServer(ourListenerServer);
|
||||
String ourBase = "http://localhost:" + ourListenerPort + "/";
|
||||
ourCtx.getRestfulClientFactory().setSocketTimeout(120000);
|
||||
ourClient = ourCtx.newRestfulGenericClient(ourBase);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -76,6 +76,7 @@ public class HashMapResourceProviderExtension<T extends IBaseResource> extends H
|
|||
myRestfulServerExtension.getRestfulServer().registerProvider(HashMapResourceProviderExtension.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized MethodOutcome update(T theResource, String theConditional, RequestDetails theRequestDetails) {
|
||||
T resourceClone = getFhirContext().newTerser().clone(theResource);
|
||||
myUpdates.add(resourceClone);
|
||||
|
|
|
@ -30,6 +30,9 @@ import ca.uhn.fhir.rest.client.api.IGenericClient;
|
|||
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
|
||||
import ca.uhn.fhir.rest.server.IPagingProvider;
|
||||
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||
import ca.uhn.fhir.rest.server.interceptor.ResponseValidatingInterceptor;
|
||||
import ca.uhn.fhir.validation.FhirValidator;
|
||||
import ca.uhn.fhir.validation.ResultSeverityEnum;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
|
|
|
@ -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>
|
||||
|
||||
<parent>
|
||||
|
@ -98,19 +99,6 @@
|
|||
<artifactId>jakarta.annotation-api</artifactId>
|
||||
</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 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
|
@ -189,7 +177,12 @@
|
|||
<artifactId>popper.js</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- Test Dependencies -->
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-servlets</artifactId>
|
||||
|
@ -220,6 +213,22 @@
|
|||
<artifactId>commons-dbcp2</artifactId>
|
||||
<scope>test</scope>
|
||||
</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>
|
||||
|
||||
<build>
|
||||
|
@ -235,9 +244,11 @@
|
|||
</goals>
|
||||
<configuration>
|
||||
<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>
|
||||
<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>
|
||||
</copy>
|
||||
</target>
|
||||
|
@ -262,17 +273,17 @@
|
|||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>jar-no-fork</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>jar-no-fork</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</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.impl.GenericClient;
|
||||
import ca.uhn.fhir.to.model.HomeRequest;
|
||||
import ca.uhn.fhir.util.BundleUtil;
|
||||
import ca.uhn.fhir.util.ExtensionConstants;
|
||||
import ca.uhn.hapi.converters.canonical.VersionCanonicalizer;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
@ -21,8 +22,10 @@ import org.apache.http.Header;
|
|||
import org.apache.http.entity.ContentType;
|
||||
import org.apache.http.message.BasicHeader;
|
||||
import org.hl7.fhir.instance.model.api.IAnyResource;
|
||||
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
||||
import org.hl7.fhir.instance.model.api.IBaseConformance;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IBaseXhtml;
|
||||
import org.hl7.fhir.instance.model.api.IDomainResource;
|
||||
import org.hl7.fhir.r5.model.CapabilityStatement;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
@ -373,23 +376,38 @@ public class BaseController {
|
|||
|
||||
private String parseNarrative(HomeRequest theRequest, EncodingEnum theCtEnum, String theResultBody) {
|
||||
try {
|
||||
IBaseResource par = theCtEnum.newParser(getContext(theRequest)).parseResource(theResultBody);
|
||||
String retVal;
|
||||
if (par instanceof IResource) {
|
||||
IResource resource = (IResource) par;
|
||||
retVal = resource.getText().getDiv().getValueAsString();
|
||||
} else if (par instanceof IDomainResource) {
|
||||
retVal = ((IDomainResource) par).getText().getDivAsString();
|
||||
} else {
|
||||
retVal = null;
|
||||
}
|
||||
return StringUtils.defaultString(retVal);
|
||||
FhirContext context = getContext(theRequest);
|
||||
IBaseResource result = theCtEnum.newParser(context).parseResource(theResultBody);
|
||||
return parseNarrative(context, result);
|
||||
} catch (Exception e) {
|
||||
ourLog.error("Failed to parse resource", e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private String parseNarrative(FhirContext theContext, IBaseResource theResult) throws Exception {
|
||||
String retVal = null;
|
||||
if (theResult instanceof IResource) {
|
||||
IResource resource = (IResource) theResult;
|
||||
retVal = resource.getText().getDiv().getValueAsString();
|
||||
} else if (theResult instanceof IDomainResource) {
|
||||
retVal = ((IDomainResource) theResult).getText().getDivAsString();
|
||||
} else if (theResult instanceof IBaseBundle) {
|
||||
// If this is a document, we'll pull the narrative from the Composition
|
||||
IBaseBundle bundle = (IBaseBundle) theResult;
|
||||
if ("document".equals(BundleUtil.getBundleType(theContext, bundle))) {
|
||||
IBaseResource firstResource = theContext.newTerser().getSingleValueOrNull(bundle, "Bundle.entry.resource", IBaseResource.class);
|
||||
if (firstResource != null && "Composition".equals(theContext.getResourceType(firstResource))) {
|
||||
IBaseXhtml html = theContext.newTerser().getSingleValueOrNull(firstResource, "text.div", IBaseXhtml.class);
|
||||
if (html != null) {
|
||||
retVal = html.getValueAsString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return StringUtils.defaultString(retVal);
|
||||
}
|
||||
|
||||
protected String preProcessMessageBody(String theBody) {
|
||||
if (theBody == null) {
|
||||
return "";
|
||||
|
@ -462,7 +480,6 @@ public class BaseController {
|
|||
switch (ctEnum) {
|
||||
case JSON:
|
||||
if (theResultType == ResultType.RESOURCE) {
|
||||
narrativeString = parseNarrative(theRequest, ctEnum, resultBody);
|
||||
resultDescription.append("JSON resource");
|
||||
} else if (theResultType == ResultType.BUNDLE) {
|
||||
resultDescription.append("JSON bundle");
|
||||
|
@ -472,7 +489,6 @@ public class BaseController {
|
|||
case XML:
|
||||
default:
|
||||
if (theResultType == ResultType.RESOURCE) {
|
||||
narrativeString = parseNarrative(theRequest, ctEnum, resultBody);
|
||||
resultDescription.append("XML resource");
|
||||
} else if (theResultType == ResultType.BUNDLE) {
|
||||
resultDescription.append("XML bundle");
|
||||
|
@ -480,6 +496,7 @@ public class BaseController {
|
|||
}
|
||||
break;
|
||||
}
|
||||
narrativeString = parseNarrative(theRequest, ctEnum, resultBody);
|
||||
}
|
||||
|
||||
resultDescription.append(" (").append(defaultString(resultBody).length() + " bytes)");
|
||||
|
@ -508,6 +525,9 @@ public class BaseController {
|
|||
theModelMap.put("narrative", narrativeString);
|
||||
theModelMap.put("latencyMs", theLatency);
|
||||
|
||||
theModelMap.put("config", myConfig);
|
||||
theModelMap.put("serverId", theRequest.getServerId());
|
||||
|
||||
} catch (Exception e) {
|
||||
ourLog.error("Failure during processing", e);
|
||||
theModelMap.put("errorMsg", toDisplayError("Error during processing: " + e.getMessage(), e));
|
||||
|
|
|
@ -25,10 +25,11 @@ import ca.uhn.fhir.rest.gclient.QuantityClientParam;
|
|||
import ca.uhn.fhir.rest.gclient.QuantityClientParam.IAndUnits;
|
||||
import ca.uhn.fhir.rest.gclient.StringClientParam;
|
||||
import ca.uhn.fhir.rest.gclient.TokenClientParam;
|
||||
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
|
||||
import ca.uhn.fhir.to.model.HomeRequest;
|
||||
import ca.uhn.fhir.to.model.ResourceRequest;
|
||||
import ca.uhn.fhir.to.model.TransactionRequest;
|
||||
import ca.uhn.fhir.util.UrlUtil;
|
||||
import ca.uhn.fhir.util.StopWatch;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hl7.fhir.dstu3.model.CapabilityStatement;
|
||||
|
@ -38,6 +39,7 @@ import org.hl7.fhir.dstu3.model.CapabilityStatement.CapabilityStatementRestResou
|
|||
import org.hl7.fhir.dstu3.model.StringType;
|
||||
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
||||
import org.hl7.fhir.instance.model.api.IBaseConformance;
|
||||
import org.hl7.fhir.instance.model.api.IBaseParameters;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.validation.BindingResult;
|
||||
|
@ -599,6 +601,53 @@ public class Controller extends BaseController {
|
|||
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@RequestMapping(value = { "/operation" })
|
||||
public String actionOperation(final HttpServletRequest theReq, final HomeRequest theRequest, final BindingResult theBindingResult, final ModelMap theModel) {
|
||||
|
||||
String instanceType = theReq.getParameter("instanceType");
|
||||
String instanceId = theReq.getParameter("instanceId");
|
||||
String operationName = theReq.getParameter("operationName");
|
||||
|
||||
boolean finished = false;
|
||||
|
||||
addCommonParams(theReq, theRequest, theModel);
|
||||
|
||||
CaptureInterceptor interceptor = new CaptureInterceptor();
|
||||
GenericClient client = theRequest.newClient(theReq, getContext(theRequest), myConfig, interceptor);
|
||||
client.setPrettyPrint(true);
|
||||
|
||||
Class<? 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) {
|
||||
addCommonParams(theReq, theRequest, theModel);
|
||||
|
||||
|
|
|
@ -8,20 +8,22 @@ import org.springframework.context.annotation.ComponentScan;
|
|||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
|
||||
import org.thymeleaf.spring5.SpringTemplateEngine;
|
||||
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
|
||||
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
|
||||
import org.thymeleaf.templatemode.TemplateMode;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
@ComponentScan(basePackages = "ca.uhn.fhir.to")
|
||||
public class FhirTesterMvcConfig extends WebMvcConfigurerAdapter {
|
||||
public class FhirTesterMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry theRegistry) {
|
||||
public void addResourceHandlers(@Nonnull ResourceHandlerRegistry theRegistry) {
|
||||
WebUtil.webJarAddBoostrap(theRegistry);
|
||||
WebUtil.webJarAddJQuery(theRegistry);
|
||||
WebUtil.webJarAddFontAwesome(theRegistry);
|
||||
|
@ -40,13 +42,17 @@ public class FhirTesterMvcConfig extends WebMvcConfigurerAdapter {
|
|||
}
|
||||
|
||||
@Bean
|
||||
public SpringResourceTemplateResolver templateResolver() {
|
||||
public SpringResourceTemplateResolver templateResolver(TesterConfig theTesterConfig) {
|
||||
SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
|
||||
resolver.setPrefix("/WEB-INF/templates/");
|
||||
resolver.setSuffix(".html");
|
||||
resolver.setTemplateMode(TemplateMode.HTML);
|
||||
resolver.setCharacterEncoding("UTF-8");
|
||||
|
||||
if (theTesterConfig.getDebugTemplatesMode()) {
|
||||
resolver.setCacheable(false);
|
||||
}
|
||||
|
||||
return resolver;
|
||||
}
|
||||
|
||||
|
@ -56,17 +62,17 @@ public class FhirTesterMvcConfig extends WebMvcConfigurerAdapter {
|
|||
}
|
||||
|
||||
@Bean
|
||||
public ThymeleafViewResolver viewResolver() {
|
||||
public ThymeleafViewResolver viewResolver(SpringTemplateEngine theTemplateEngine) {
|
||||
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
|
||||
viewResolver.setTemplateEngine(templateEngine());
|
||||
viewResolver.setTemplateEngine(theTemplateEngine);
|
||||
viewResolver.setCharacterEncoding("UTF-8");
|
||||
return viewResolver;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SpringTemplateEngine templateEngine() {
|
||||
public SpringTemplateEngine templateEngine(SpringResourceTemplateResolver theTemplateResolver) {
|
||||
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
|
||||
templateEngine.setTemplateResolver(templateResolver());
|
||||
templateEngine.setTemplateResolver(theTemplateResolver);
|
||||
|
||||
return templateEngine;
|
||||
}
|
||||
|
|
|
@ -1,29 +1,35 @@
|
|||
package ca.uhn.fhir.to;
|
||||
|
||||
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import java.util.*;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
||||
import ca.uhn.fhir.rest.server.util.ITestingUiClientFactory;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.springframework.beans.factory.annotation.Required;
|
||||
|
||||
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||
import ca.uhn.fhir.rest.server.util.ITestingUiClientFactory;
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class TesterConfig {
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TesterConfig.class);
|
||||
|
||||
public static final String SYSPROP_FORCE_SERVERS = "ca.uhn.fhir.to.TesterConfig_SYSPROP_FORCE_SERVERS";
|
||||
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TesterConfig.class);
|
||||
private final LinkedHashMap<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 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 List<ServerBuilder> myServerBuilders = new ArrayList<TesterConfig.ServerBuilder>();
|
||||
private boolean myDebugTemplatesMode;
|
||||
|
||||
public IServerBuilderStep1 addServer() {
|
||||
ServerBuilder retVal = new ServerBuilder();
|
||||
|
@ -42,6 +48,11 @@ public class TesterConfig {
|
|||
myIdToServerBase.put(next.myId, next.myBaseUrl);
|
||||
myIdToServerName.put(next.myId, next.myName);
|
||||
myIdToAllowsApiKey.put(next.myId, next.myAllowsApiKey);
|
||||
myServerIdToTypeToOperationNameToInclusionChecker.put(next.myId, next.myOperationNameToInclusionChecker);
|
||||
myServerIdToTypeToInteractionNameToInclusionChecker.put(next.myId, next.mySearchResultRowInteractionEnabled);
|
||||
if (next.myEnableDebugTemplates) {
|
||||
myDebugTemplatesMode = true;
|
||||
}
|
||||
}
|
||||
myServerBuilders.clear();
|
||||
}
|
||||
|
@ -50,8 +61,12 @@ public class TesterConfig {
|
|||
return myClientFactory;
|
||||
}
|
||||
|
||||
public void setClientFactory(ITestingUiClientFactory theClientFactory) {
|
||||
myClientFactory = theClientFactory;
|
||||
}
|
||||
|
||||
public boolean getDebugTemplatesMode() {
|
||||
return true;
|
||||
return myDebugTemplatesMode;
|
||||
}
|
||||
|
||||
public LinkedHashMap<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
|
||||
* response payloads the refer to third party servers (e.g. paging URLs etc)
|
||||
* response payloads that refer to third party servers (e.g. paging URLs etc)
|
||||
*/
|
||||
public boolean isRefuseToFetchThirdPartyUrls() {
|
||||
return myRefuseToFetchThirdPartyUrls;
|
||||
}
|
||||
|
||||
public void setClientFactory(ITestingUiClientFactory theClientFactory) {
|
||||
myClientFactory = theClientFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* If set to {@literal true} (default is true) the server will refuse to load URLs in
|
||||
* response payloads the refer to third party servers (e.g. paging URLs etc)
|
||||
* response payloads that refer to third party servers (e.g. paging URLs etc)
|
||||
*/
|
||||
public void setRefuseToFetchThirdPartyUrls(boolean theRefuseToFetchThirdPartyUrls) {
|
||||
myRefuseToFetchThirdPartyUrls = theRefuseToFetchThirdPartyUrls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from Thymeleaf
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public List<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
|
||||
public void setServers(List<String> theServers) {
|
||||
List<String> servers = theServers;
|
||||
|
@ -97,7 +140,7 @@ public class TesterConfig {
|
|||
// This is mostly for unit tests
|
||||
String force = System.getProperty(SYSPROP_FORCE_SERVERS);
|
||||
if (StringUtils.isNotBlank(force)) {
|
||||
ourLog.warn("Forcing server confirguration because of system property: {}", force);
|
||||
ourLog.warn("Forcing server configuration because of system property: {}", force);
|
||||
servers = Collections.singletonList(force);
|
||||
}
|
||||
|
||||
|
@ -148,15 +191,49 @@ public class TesterConfig {
|
|||
|
||||
IServerBuilderStep5 allowsApiKey();
|
||||
|
||||
/**
|
||||
* If this is set, Thymeleaf UI templates will be run in debug mode, meaning
|
||||
* no caching between executions. This is helpful if you want to make live changes
|
||||
* to the template.
|
||||
*
|
||||
* @since 6.4.0
|
||||
*/
|
||||
IServerBuilderStep5 enableDebugTemplates();
|
||||
|
||||
/**
|
||||
* Use this method to add buttons to invoke operations on the search result table.
|
||||
*/
|
||||
ServerBuilder withSearchResultRowOperation(String theOperationName, IInclusionChecker theInclusionChecker);
|
||||
|
||||
/**
|
||||
* Use this method to enable/disable the interaction buttons on the search result rows table.
|
||||
* By default {@link RestOperationTypeEnum#READ} and {@link RestOperationTypeEnum#UPDATE} are
|
||||
* already enabled, and they are currently the only interactions supported.
|
||||
*/
|
||||
ServerBuilder withSearchResultRowInteraction(RestOperationTypeEnum theInteraction, IInclusionChecker theEnabled);
|
||||
}
|
||||
|
||||
public interface IInclusionChecker {
|
||||
|
||||
boolean shouldInclude(IIdType theResourceId);
|
||||
|
||||
}
|
||||
|
||||
public class ServerBuilder implements IServerBuilderStep1, IServerBuilderStep2, IServerBuilderStep3, IServerBuilderStep4, IServerBuilderStep5 {
|
||||
|
||||
private final Map<String, IInclusionChecker> myOperationNameToInclusionChecker = new LinkedHashMap<>();
|
||||
private final Map<RestOperationTypeEnum, IInclusionChecker> mySearchResultRowInteractionEnabled = new LinkedHashMap<>();
|
||||
private boolean myAllowsApiKey;
|
||||
private String myBaseUrl;
|
||||
private String myId;
|
||||
private String myName;
|
||||
private FhirVersionEnum myVersion;
|
||||
private boolean myEnableDebugTemplates;
|
||||
|
||||
public ServerBuilder() {
|
||||
mySearchResultRowInteractionEnabled.put(RestOperationTypeEnum.READ, id -> true);
|
||||
mySearchResultRowInteractionEnabled.put(RestOperationTypeEnum.UPDATE, id -> true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IServerBuilderStep1 addServer() {
|
||||
|
@ -171,6 +248,24 @@ public class TesterConfig {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IServerBuilderStep5 enableDebugTemplates() {
|
||||
myEnableDebugTemplates = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerBuilder withSearchResultRowOperation(String theOperationName, IInclusionChecker theResourceType) {
|
||||
myOperationNameToInclusionChecker.put(theOperationName, theResourceType);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerBuilder withSearchResultRowInteraction(RestOperationTypeEnum theInteraction, IInclusionChecker theEnabled) {
|
||||
mySearchResultRowInteractionEnabled.put(theInteraction, theEnabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IServerBuilderStep4 withBaseUrl(String theBaseUrl) {
|
||||
Validate.notBlank(theBaseUrl, "theBaseUrl can not be blank");
|
||||
|
@ -200,5 +295,4 @@ public class TesterConfig {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -117,6 +117,10 @@
|
|||
</div>
|
||||
</td>
|
||||
</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)}">
|
||||
<td rowspan="2">
|
||||
Result Body
|
||||
|
@ -124,239 +128,18 @@
|
|||
</td>
|
||||
<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
|
||||
<th:block th:if="${bundle} != null">
|
||||
<th:block th:replace="tmpl-result-controltable-hapi :: controltable"></th:block>
|
||||
</th:block>
|
||||
<th:block th:if="${riBundle} != null AND ( ${riBundle.type.name()} == 'SEARCHSET' OR ${riBundle.type.name()} == 'HISTORY' )">
|
||||
<th:block th:replace="tmpl-result-controltable-ri :: controltable"></th:block>
|
||||
</th:block>
|
||||
|
||||
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}">
|
||||
|
||||
<!-- 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 class="panel-heading">
|
||||
<div class="panel-title-text">
|
||||
Payload
|
||||
</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>
|
||||
</tr>
|
||||
<tr th:if="${!#strings.isEmpty(resultBody)}">
|
||||
|
@ -367,10 +150,6 @@
|
|||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${!#strings.isEmpty(narrative)}">
|
||||
<td class="propertyKeyCell">Result Narrative</td>
|
||||
<td th:utext="${narrative}"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</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;
|
||||
}
|
||||
|
||||
H1 {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
H2 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
H3 {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
H4 {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.clientCodeBox {
|
||||
font-family: monospace;
|
||||
font-size: 0.8em;
|
||||
|
@ -53,6 +66,14 @@ label {
|
|||
line-height: 0.8em;
|
||||
}
|
||||
|
||||
TD.resultControlButtons {
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
TD.resultControlButtons BUTTON {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
TD.headerBox {
|
||||
line-height: 0.8em !important;
|
||||
}
|
||||
|
@ -153,10 +174,12 @@ DIV.navbarBreadcrumb:HOVER, A.navbarBreadcrumb:HOVER {
|
|||
}
|
||||
|
||||
DIV.resultBodyActual {
|
||||
/*
|
||||
max-height: 400px;
|
||||
overflow: scroll;
|
||||
*/
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
DIV.panel-title-text {
|
||||
font-weight: bold;
|
||||
margin: 12px;
|
||||
}
|
||||
|
||||
PRE.resultBodyPre {
|
||||
|
|
|
@ -509,6 +509,18 @@ function updateFromEntriesTable(source, type, id, vid) {
|
|||
$("#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
|
||||
|
|
|
@ -8,7 +8,6 @@ import ca.uhn.fhir.context.FhirVersionEnum;
|
|||
import ca.uhn.fhir.to.FhirTesterMvcConfig;
|
||||
import ca.uhn.fhir.to.TesterConfig;
|
||||
|
||||
//@formatter:off
|
||||
/**
|
||||
* This spring config file configures the web testing module. It serves two
|
||||
* purposes:
|
||||
|
@ -41,7 +40,7 @@ public class FhirTesterConfig {
|
|||
retVal
|
||||
.addServer()
|
||||
.withId("internal")
|
||||
.withFhirVersion(FhirVersionEnum.DSTU2)
|
||||
.withFhirVersion(FhirVersionEnum.R4)
|
||||
.withBaseUrl("http://localhost:8888/fhir")
|
||||
.withName("Localhost Server")
|
||||
.allowsApiKey()
|
||||
|
@ -65,4 +64,3 @@ public class FhirTesterConfig {
|
|||
}
|
||||
|
||||
}
|
||||
//@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