Merge branch 'master' of github.com:jamesagnew/hapi-fhir

This commit is contained in:
James Agnew 2019-10-25 17:05:30 -04:00
commit c83b330c48
8 changed files with 193 additions and 82 deletions

View File

@ -155,6 +155,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
@Override @Override
public void cancelAllActiveSearches() { public void cancelAllActiveSearches() {
for (SearchTask next : myIdToSearchTask.values()) { for (SearchTask next : myIdToSearchTask.values()) {
ourLog.info("Requesting immediate abort of search: {}", next.getSearch().getUuid());
next.requestImmediateAbort(); next.requestImmediateAbort();
AsyncUtil.awaitLatchAndIgnoreInterrupt(next.getCompletionLatch(), 30, TimeUnit.SECONDS); AsyncUtil.awaitLatchAndIgnoreInterrupt(next.getCompletionLatch(), 30, TimeUnit.SECONDS);
} }
@ -631,6 +632,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
Integer awaitInitialSync() { Integer awaitInitialSync() {
ourLog.trace("Awaiting initial sync"); ourLog.trace("Awaiting initial sync");
do { do {
ourLog.trace("Search {} aborted: {}", getSearch().getUuid(), !isNotAborted());
if (AsyncUtil.awaitLatchAndThrowInternalErrorExceptionOnInterrupt(getInitialCollectionLatch(), 250L, TimeUnit.MILLISECONDS)) { if (AsyncUtil.awaitLatchAndThrowInternalErrorExceptionOnInterrupt(getInitialCollectionLatch(), 250L, TimeUnit.MILLISECONDS)) {
break; break;
} }

View File

@ -8,8 +8,10 @@ import ca.uhn.fhir.rest.client.apache.ResourceEntity;
import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.test.utilities.JettyUtil; import ca.uhn.fhir.test.utilities.JettyUtil;
import ca.uhn.fhir.util.UrlUtil;
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPost;
@ -96,7 +98,7 @@ public class BulkDataExportProviderTest {
} }
@Test @Test
public void testSuccessfulInitiateBulkRequest() throws IOException { public void testSuccessfulInitiateBulkRequest_Post() throws IOException {
IBulkDataExportSvc.JobInfo jobInfo = new IBulkDataExportSvc.JobInfo() IBulkDataExportSvc.JobInfo jobInfo = new IBulkDataExportSvc.JobInfo()
.setJobId(A_JOB_ID); .setJobId(A_JOB_ID);
@ -110,9 +112,12 @@ public class BulkDataExportProviderTest {
input.addParameter(JpaConstants.PARAM_EXPORT_SINCE, now); input.addParameter(JpaConstants.PARAM_EXPORT_SINCE, now);
input.addParameter(JpaConstants.PARAM_EXPORT_TYPE_FILTER, new StringType("Patient?identifier=foo")); input.addParameter(JpaConstants.PARAM_EXPORT_TYPE_FILTER, new StringType("Patient?identifier=foo"));
ourLog.info(myCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(input));
HttpPost post = new HttpPost("http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT); HttpPost post = new HttpPost("http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT);
post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC); post.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
post.setEntity(new ResourceEntity(myCtx, input)); post.setEntity(new ResourceEntity(myCtx, input));
ourLog.info("Request: {}", post);
try (CloseableHttpResponse response = myClient.execute(post)) { try (CloseableHttpResponse response = myClient.execute(post)) {
ourLog.info("Response: {}", response.toString()); ourLog.info("Response: {}", response.toString());
@ -129,6 +134,40 @@ public class BulkDataExportProviderTest {
} }
@Test
public void testSuccessfulInitiateBulkRequest_Get() throws IOException {
IBulkDataExportSvc.JobInfo jobInfo = new IBulkDataExportSvc.JobInfo()
.setJobId(A_JOB_ID);
when(myBulkDataExportSvc.submitJob(any(), any(), any(), any())).thenReturn(jobInfo);
InstantType now = InstantType.now();
String url = "http://localhost:" + myPort + "/" + JpaConstants.OPERATION_EXPORT
+ "?" + JpaConstants.PARAM_EXPORT_OUTPUT_FORMAT + "=" + UrlUtil.escapeUrlParam(Constants.CT_FHIR_NDJSON)
+ "&" + JpaConstants.PARAM_EXPORT_TYPE + "=" + UrlUtil.escapeUrlParam("Patient, Practitioner")
+ "&" + JpaConstants.PARAM_EXPORT_SINCE+ "="+ UrlUtil.escapeUrlParam(now.getValueAsString())
+ "&" + JpaConstants.PARAM_EXPORT_TYPE_FILTER + "=" + UrlUtil.escapeUrlParam("Patient?identifier=foo");
HttpGet get = new HttpGet(url);
get.addHeader(Constants.HEADER_PREFER, Constants.HEADER_PREFER_RESPOND_ASYNC);
ourLog.info("Request: {}", url);
try (CloseableHttpResponse response = myClient.execute(get)) {
ourLog.info("Response: {}", response.toString());
assertEquals(202, response.getStatusLine().getStatusCode());
assertEquals("Accepted", response.getStatusLine().getReasonPhrase());
assertEquals("http://localhost:" + myPort + "/$export-poll-status?_jobId=" + A_JOB_ID, response.getFirstHeader(Constants.HEADER_CONTENT_LOCATION).getValue());
}
verify(myBulkDataExportSvc, times(1)).submitJob(myOutputFormatCaptor.capture(), myResourceTypesCaptor.capture(), mySinceCaptor.capture(), myFiltersCaptor.capture());
assertEquals(Constants.CT_FHIR_NDJSON, myOutputFormatCaptor.getValue());
assertThat(myResourceTypesCaptor.getValue(), containsInAnyOrder("Patient", "Practitioner"));
assertThat(mySinceCaptor.getValue(), notNullValue());
assertThat(myFiltersCaptor.getValue(), containsInAnyOrder("Patient?identifier=foo"));
}
@Test @Test
public void testPollForStatus_BUILDING() throws IOException { public void testPollForStatus_BUILDING() throws IOException {

View File

@ -33,6 +33,7 @@ import java.util.Date;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import static ca.uhn.fhir.jpa.search.cache.DatabaseSearchCacheSvcImpl.DEFAULT_CUTOFF_SLACK; import static ca.uhn.fhir.jpa.search.cache.DatabaseSearchCacheSvcImpl.DEFAULT_CUTOFF_SLACK;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -165,13 +166,10 @@ public class FhirResourceDaoR4SearchPageExpiryTest extends BaseJpaR4Test {
DatabaseSearchCacheSvcImpl.setNowForUnitTests(search3timestamp.get() + 2100); DatabaseSearchCacheSvcImpl.setNowForUnitTests(search3timestamp.get() + 2100);
myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem(); myStaleSearchDeletingSvc.pollForStaleSearchesAndDeleteThem();
newTxTemplate().execute(new TransactionCallbackWithoutResult() {
@Override await().until(()-> newTxTemplate().execute(t -> !mySearchEntityDao.findByUuidAndFetchIncludes(searchUuid1).isPresent()));
protected void doInTransactionWithoutResult(TransactionStatus theArg0) { await().until(()-> newTxTemplate().execute(t -> !mySearchEntityDao.findByUuidAndFetchIncludes(searchUuid3).isPresent()));
assertFalse("Search 1 still exists", mySearchEntityDao.findByUuidAndFetchIncludes(searchUuid1).isPresent());
assertFalse("Search 3 still exists", mySearchEntityDao.findByUuidAndFetchIncludes(searchUuid3).isPresent());
}
});
} }

View File

@ -26,9 +26,7 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.stubbing.Answer; import org.mockito.stubbing.Answer;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -39,8 +37,12 @@ import org.springframework.transaction.TransactionStatus;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import java.util.*; import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast; import static ca.uhn.fhir.jpa.util.TestUtil.sleepAtLeast;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.*; import static org.mockito.ArgumentMatchers.*;
@ -108,9 +110,9 @@ public class SearchCoordinatorSvcImplTest {
}).when(myCallingDao).injectDependenciesIntoBundleProvider(any(PersistedJpaBundleProvider.class)); }).when(myCallingDao).injectDependenciesIntoBundleProvider(any(PersistedJpaBundleProvider.class));
} }
private List<Long> createPidSequence(int from, int to) { private List<Long> createPidSequence(int to) {
List<Long> pids = new ArrayList<>(); List<Long> pids = new ArrayList<>();
for (long i = from; i < to; i++) { for (long i = 10; i < to; i++) {
pids.add(i); pids.add(i);
} }
return pids; return pids;
@ -134,7 +136,7 @@ public class SearchCoordinatorSvcImplTest {
SearchParameterMap params = new SearchParameterMap(); SearchParameterMap params = new SearchParameterMap();
params.add("name", new StringParam("ANAME")); params.add("name", new StringParam("ANAME"));
List<Long> pids = createPidSequence(10, 800); List<Long> pids = createPidSequence(800);
IResultIterator iter = new FailAfterNIterator(new SlowIterator(pids.iterator(), 2), 300); IResultIterator iter = new FailAfterNIterator(new SlowIterator(pids.iterator(), 2), 300);
when(mySearchBuilder.createQuery(same(params), any(), any())).thenReturn(iter); when(mySearchBuilder.createQuery(same(params), any(), any())).thenReturn(iter);
@ -166,7 +168,7 @@ public class SearchCoordinatorSvcImplTest {
SearchParameterMap params = new SearchParameterMap(); SearchParameterMap params = new SearchParameterMap();
params.add("name", new StringParam("ANAME")); params.add("name", new StringParam("ANAME"));
List<Long> pids = createPidSequence(10, 800); List<Long> pids = createPidSequence(800);
SlowIterator iter = new SlowIterator(pids.iterator(), 1); SlowIterator iter = new SlowIterator(pids.iterator(), 1);
when(mySearchBuilder.createQuery(any(), any(), any())).thenReturn(iter); when(mySearchBuilder.createQuery(any(), any(), any())).thenReturn(iter);
doAnswer(loadPids()).when(mySearchBuilder).loadResourcesByPid(any(Collection.class), any(Collection.class), any(List.class), anyBoolean(), any()); doAnswer(loadPids()).when(mySearchBuilder).loadResourcesByPid(any(Collection.class), any(Collection.class), any(List.class), anyBoolean(), any());
@ -203,9 +205,7 @@ public class SearchCoordinatorSvcImplTest {
myCurrentSearch = search; myCurrentSearch = search;
return search; return search;
}); });
when(mySearchCacheSvc.fetchByUuid(any())).thenAnswer(t -> { when(mySearchCacheSvc.fetchByUuid(any())).thenAnswer(t -> Optional.ofNullable(myCurrentSearch));
return Optional.ofNullable(myCurrentSearch);
});
IFhirResourceDao dao = myCallingDao; IFhirResourceDao dao = myCallingDao;
when(myDaoRegistry.getResourceDao(any(String.class))).thenReturn(dao); when(myDaoRegistry.getResourceDao(any(String.class))).thenReturn(dao);
@ -274,7 +274,7 @@ public class SearchCoordinatorSvcImplTest {
SearchParameterMap params = new SearchParameterMap(); SearchParameterMap params = new SearchParameterMap();
params.add("name", new StringParam("ANAME")); params.add("name", new StringParam("ANAME"));
List<Long> pids = createPidSequence(10, 800); List<Long> pids = createPidSequence(800);
SlowIterator iter = new SlowIterator(pids.iterator(), 2); SlowIterator iter = new SlowIterator(pids.iterator(), 2);
when(mySearchBuilder.createQuery(same(params), any(), any())).thenReturn(iter); when(mySearchBuilder.createQuery(same(params), any(), any())).thenReturn(iter);
@ -294,32 +294,43 @@ public class SearchCoordinatorSvcImplTest {
} }
@Test @Test
public void testCancelActiveSearches() { public void testCancelActiveSearches() throws InterruptedException {
SearchParameterMap params = new SearchParameterMap(); SearchParameterMap params = new SearchParameterMap();
params.add("name", new StringParam("ANAME")); params.add("name", new StringParam("ANAME"));
List<Long> pids = createPidSequence(10, 400); List<Long> pids = createPidSequence(800);
SlowIterator iter = new SlowIterator(pids.iterator(), 50); SlowIterator iter = new SlowIterator(pids.iterator(), 500);
when(mySearchBuilder.createQuery(same(params), any(), any())).thenReturn(iter); when(mySearchBuilder.createQuery(same(params), any(), any())).thenReturn(iter);
doAnswer(loadPids()).when(mySearchBuilder).loadResourcesByPid(any(Collection.class), any(Collection.class), any(List.class), anyBoolean(), any());
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective(), null); IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective(), null);
assertNotNull(result.getUuid()); assertNotNull(result.getUuid());
assertEquals(null, result.size());
List<IBaseResource> resources; CountDownLatch completionLatch = new CountDownLatch(1);
Runnable taskStarter = () -> {
resources = result.getResources(0, 1); try {
ourLog.info("About to pull the first resource");
List<IBaseResource> resources = result.getResources(0, 1);
ourLog.info("Done pulling the first resource");
assertEquals(1, resources.size()); assertEquals(1, resources.size());
} finally {
completionLatch.countDown();
}
};
new Thread(taskStarter).start();
await().until(()->iter.getCountReturned() >= 3);
ourLog.info("About to cancel all searches");
mySvc.cancelAllActiveSearches(); mySvc.cancelAllActiveSearches();
ourLog.info("Done cancelling all searches");
try { try {
result.getResources(10, 20); result.getResources(10, 20);
} catch (InternalErrorException e) { } catch (InternalErrorException e) {
assertEquals("Abort has been requested", e.getMessage()); assertEquals("Abort has been requested", e.getMessage());
} }
completionLatch.await(10, TimeUnit.SECONDS);
} }
/** /**
@ -331,7 +342,7 @@ public class SearchCoordinatorSvcImplTest {
SearchParameterMap params = new SearchParameterMap(); SearchParameterMap params = new SearchParameterMap();
params.add("name", new StringParam("ANAME")); params.add("name", new StringParam("ANAME"));
List<Long> pids = createPidSequence(10, 800); List<Long> pids = createPidSequence(800);
IResultIterator iter = new SlowIterator(pids.iterator(), 2); IResultIterator iter = new SlowIterator(pids.iterator(), 2);
when(mySearchBuilder.createQuery(same(params), any(), any())).thenReturn(iter); when(mySearchBuilder.createQuery(same(params), any(), any())).thenReturn(iter);
when(mySearchCacheSvc.save(any())).thenAnswer(t -> t.getArguments()[0]); when(mySearchCacheSvc.save(any())).thenAnswer(t -> t.getArguments()[0]);
@ -376,7 +387,7 @@ public class SearchCoordinatorSvcImplTest {
SearchParameterMap params = new SearchParameterMap(); SearchParameterMap params = new SearchParameterMap();
params.add("name", new StringParam("ANAME")); params.add("name", new StringParam("ANAME"));
List<Long> pids = createPidSequence(10, 100); List<Long> pids = createPidSequence(100);
SlowIterator iter = new SlowIterator(pids.iterator(), 2); SlowIterator iter = new SlowIterator(pids.iterator(), 2);
when(mySearchBuilder.createQuery(same(params), any(), any())).thenReturn(iter); when(mySearchBuilder.createQuery(same(params), any(), any())).thenReturn(iter);
@ -384,7 +395,7 @@ public class SearchCoordinatorSvcImplTest {
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective(), null); IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective(), null);
assertNotNull(result.getUuid()); assertNotNull(result.getUuid());
assertEquals(90, result.size().intValue()); assertEquals(90, Objects.requireNonNull(result.size()).intValue());
List<IBaseResource> resources = result.getResources(0, 30); List<IBaseResource> resources = result.getResources(0, 30);
assertEquals(30, resources.size()); assertEquals(30, resources.size());
@ -396,6 +407,7 @@ public class SearchCoordinatorSvcImplTest {
@Test @Test
public void testGetPage() { public void testGetPage() {
Pageable page = SearchCoordinatorSvcImpl.toPage(50, 73); Pageable page = SearchCoordinatorSvcImpl.toPage(50, 73);
assert page != null;
assertEquals(50, page.getOffset()); assertEquals(50, page.getOffset());
assertEquals(23, page.getPageSize()); assertEquals(23, page.getPageSize());
} }
@ -458,14 +470,14 @@ public class SearchCoordinatorSvcImplTest {
params.setLoadSynchronous(true); params.setLoadSynchronous(true);
params.add("name", new StringParam("ANAME")); params.add("name", new StringParam("ANAME"));
List<Long> pids = createPidSequence(10, 800); List<Long> pids = createPidSequence(800);
when(mySearchBuilder.createQuery(same(params), any(), any())).thenReturn(new ResultIterator(pids.iterator())); when(mySearchBuilder.createQuery(same(params), any(), any())).thenReturn(new ResultIterator(pids.iterator()));
doAnswer(loadPids()).when(mySearchBuilder).loadResourcesByPid(any(Collection.class), any(Collection.class), any(List.class), anyBoolean(), any()); doAnswer(loadPids()).when(mySearchBuilder).loadResourcesByPid(any(Collection.class), any(Collection.class), any(List.class), anyBoolean(), any());
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective(), null); IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective(), null);
assertNull(result.getUuid()); assertNull(result.getUuid());
assertEquals(790, result.size().intValue()); assertEquals(790, Objects.requireNonNull(result.size()).intValue());
List<IBaseResource> resources = result.getResources(0, 10000); List<IBaseResource> resources = result.getResources(0, 10000);
assertEquals(790, resources.size()); assertEquals(790, resources.size());
@ -479,15 +491,15 @@ public class SearchCoordinatorSvcImplTest {
params.setLoadSynchronousUpTo(100); params.setLoadSynchronousUpTo(100);
params.add("name", new StringParam("ANAME")); params.add("name", new StringParam("ANAME"));
List<Long> pids = createPidSequence(10, 800); List<Long> pids = createPidSequence(800);
when(mySearchBuilder.createQuery(same(params), any(), nullable(RequestDetails.class))).thenReturn(new ResultIterator(pids.iterator())); when(mySearchBuilder.createQuery(same(params), any(), nullable(RequestDetails.class))).thenReturn(new ResultIterator(pids.iterator()));
pids = createPidSequence(10, 110); pids = createPidSequence(110);
doAnswer(loadPids()).when(mySearchBuilder).loadResourcesByPid(eq(pids), any(Collection.class), any(List.class), anyBoolean(), nullable(RequestDetails.class)); doAnswer(loadPids()).when(mySearchBuilder).loadResourcesByPid(eq(pids), any(Collection.class), any(List.class), anyBoolean(), nullable(RequestDetails.class));
IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective(), null); IBundleProvider result = mySvc.registerSearch(myCallingDao, params, "Patient", new CacheControlDirective(), null);
assertNull(result.getUuid()); assertNull(result.getUuid());
assertEquals(100, result.size().intValue()); assertEquals(100, Objects.requireNonNull(result.size()).intValue());
List<IBaseResource> resources = result.getResources(0, 10000); List<IBaseResource> resources = result.getResources(0, 10000);
assertEquals(100, resources.size()); assertEquals(100, resources.size());
@ -628,6 +640,7 @@ public class SearchCoordinatorSvcImplTest {
private int myDelay; private int myDelay;
private Iterator<Long> myWrap; private Iterator<Long> myWrap;
private List<Long> myReturnedValues = new ArrayList<>(); private List<Long> myReturnedValues = new ArrayList<>();
private AtomicInteger myCountReturned = new AtomicInteger(0);
SlowIterator(Iterator<Long> theWrap, int theDelay) { SlowIterator(Iterator<Long> theWrap, int theDelay) {
myWrap = theWrap; myWrap = theWrap;
@ -648,6 +661,10 @@ public class SearchCoordinatorSvcImplTest {
return retVal; return retVal;
} }
public int getCountReturned() {
return myCountReturned.get();
}
@Override @Override
public Long next() { public Long next() {
try { try {
@ -657,6 +674,7 @@ public class SearchCoordinatorSvcImplTest {
} }
Long retVal = myWrap.next(); Long retVal = myWrap.next();
myReturnedValues.add(retVal); myReturnedValues.add(retVal);
myCountReturned.incrementAndGet();
return retVal; return retVal;
} }

View File

@ -1,6 +1,5 @@
package ca.uhn.fhir.test.utilities.server; package ca.uhn.fhir.test.utilities.server;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.server.provider.HashMapResourceProvider; import ca.uhn.fhir.rest.server.provider.HashMapResourceProvider;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.rules.TestRule; import org.junit.rules.TestRule;
@ -14,7 +13,6 @@ public class HashMapResourceProviderRule<T extends IBaseResource> extends HashMa
/** /**
* Constructor * Constructor
* *
* @param theFhirContext The FHIR context
* @param theResourceType The resource type to support * @param theResourceType The resource type to support
*/ */
public HashMapResourceProviderRule(RestfulServerRule theRestfulServerRule, Class<T> theResourceType) { public HashMapResourceProviderRule(RestfulServerRule theRestfulServerRule, Class<T> theResourceType) {

View File

@ -43,8 +43,8 @@
<ul class="dropdown-menu" role="menu"> <ul class="dropdown-menu" role="menu">
<li th:each="serverEntry : ${serverEntries}"> <li th:each="serverEntry : ${serverEntries}">
<a th:href="'javascript:selectServer(\'' + ${serverEntry.key} + '\');'"> <a th:href="'javascript:selectServer(\'' + ${serverEntry.key} + '\');'">
<span class="fa fa-check-square-o" th:if="${serverEntry.key} == ${serverId}"></span> <i class="far fa-check-square" th:if="${serverEntry.key} == ${serverId}"></i>
<span class="fa fa-square-o" style="color: #CCC;" th:unless="${serverEntry.key} == ${serverId}"></span> <i class="far fa-square" style="color: #CCC;" th:unless="${serverEntry.key} == ${serverId}"></i>
&nbsp;&nbsp; &nbsp;&nbsp;
<th:block th:text="${serverEntry.value}"/> <th:block th:text="${serverEntry.value}"/>
</a> </a>

View File

@ -20,10 +20,10 @@ function addSearchParamRow() {
addSearchParamRow(); addSearchParamRow();
}); });
var params = new Array(); var params = [];
conformance.rest.forEach(function(rest){ conformance.rest.forEach(function(rest){
rest.resource.forEach(function(restResource){ rest.resource.forEach(function(restResource){
if (restResource.type == resourceName) { if (restResource.type === resourceName) {
if (restResource.searchParam) { if (restResource.searchParam) {
for (var i = 0; i < restResource.searchParam.length; i++) { for (var i = 0; i < restResource.searchParam.length; i++) {
var searchParam = restResource.searchParam[i]; var searchParam = restResource.searchParam[i];
@ -63,7 +63,7 @@ function updateSearchDateQualifier(qualifierBtn, qualifierInput, qualifier) {
function addSearchControls(theConformance, theSearchParamType, theSearchParamName, theSearchParamChain, theSearchParamTarget, theContainerRowNum, theRowNum) { function addSearchControls(theConformance, theSearchParamType, theSearchParamName, theSearchParamChain, theSearchParamTarget, theContainerRowNum, theRowNum) {
var addNameAndType = true; var addNameAndType = true;
if (theSearchParamType == 'id') { if (theSearchParamType === 'id') {
$('#search-param-rowopts-' + theContainerRowNum).append( $('#search-param-rowopts-' + theContainerRowNum).append(
$('<div />', { 'class': 'col-sm-3' }).append( $('<div />', { 'class': 'col-sm-3' }).append(
$('<input />', { id: 'param.' + theRowNum + '.0', placeholder: 'id', type: 'text', 'class': 'form-control' }) $('<input />', { id: 'param.' + theRowNum + '.0', placeholder: 'id', type: 'text', 'class': 'form-control' })
@ -71,37 +71,37 @@ function addSearchControls(theConformance, theSearchParamType, theSearchParamNam
); );
} else if (theSearchParamType == 'token') { } else if (theSearchParamType == 'token') {
var tokenQualifiers = new Array(); var tokenQualifiers = [];
tokenQualifiers.push(new Object()); tokenQualifiers.push({});
tokenQualifiers[0].name='Matches'; tokenQualifiers[0].name='Matches';
tokenQualifiers[0].value=''; tokenQualifiers[0].value='';
tokenQualifiers.push(new Object()); tokenQualifiers.push({});
tokenQualifiers[1].name='Text'; tokenQualifiers[1].name='Text';
tokenQualifiers[1].value=':text'; tokenQualifiers[1].value=':text';
tokenQualifiers[1].description='The search parameter is processed as a string that searches text associated with the code/value.'; tokenQualifiers[1].description='The search parameter is processed as a string that searches text associated with the code/value.';
tokenQualifiers.push(new Object()); tokenQualifiers.push({});
tokenQualifiers[2].name='Not'; tokenQualifiers[2].name='Not';
tokenQualifiers[2].value=':not'; tokenQualifiers[2].value=':not';
tokenQualifiers[2].description='Reverse the code matching described in the paragraph above. Note that this includes resources that have no value for the parameter.'; tokenQualifiers[2].description='Reverse the code matching described in the paragraph above. Note that this includes resources that have no value for the parameter.';
tokenQualifiers.push(new Object()); tokenQualifiers.push({});
tokenQualifiers[3].name='Above'; tokenQualifiers[3].name='Above';
tokenQualifiers[3].value=':above'; tokenQualifiers[3].value=':above';
tokenQualifiers[3].description='The search parameter is a concept with the form [system]|[code], and the search parameter tests whether the coding in a resource subsumes the specified search code. For example, the search concept has an is-a relationship with the coding in the resource, and this includes the coding itself.'; tokenQualifiers[3].description='The search parameter is a concept with the form [system]|[code], and the search parameter tests whether the coding in a resource subsumes the specified search code. For example, the search concept has an is-a relationship with the coding in the resource, and this includes the coding itself.';
tokenQualifiers.push(new Object()); tokenQualifiers.push({});
tokenQualifiers[4].name='Below'; tokenQualifiers[4].name='Below';
tokenQualifiers[4].value=':below'; tokenQualifiers[4].value=':below';
tokenQualifiers[4].description='The search parameter is a concept with the form [system]|[code], and the search parameter tests whether the coding in a resource is subsumed by the specified search code. For example, the coding in the resource has an is-a relationship with the search concept, and this includes the coding itself.'; tokenQualifiers[4].description='The search parameter is a concept with the form [system]|[code], and the search parameter tests whether the coding in a resource is subsumed by the specified search code. For example, the coding in the resource has an is-a relationship with the search concept, and this includes the coding itself.';
tokenQualifiers.push(new Object()); tokenQualifiers.push({});
tokenQualifiers[5].name='In'; tokenQualifiers[5].name='In';
tokenQualifiers[5].value=':in'; tokenQualifiers[5].value=':in';
tokenQualifiers[5].description='The search parameter is a URI (relative or absolute) that identifies a value set, and the search parameter tests whether the coding is in the specified value set. The reference may be literal (to an address where the value set can be found) or logical (a reference to ValueSet.url). If the server can treat the reference as a literal URL, it does, else it tries to match known logical ValueSet.url values.'; tokenQualifiers[5].description='The search parameter is a URI (relative or absolute) that identifies a value set, and the search parameter tests whether the coding is in the specified value set. The reference may be literal (to an address where the value set can be found) or logical (a reference to ValueSet.url). If the server can treat the reference as a literal URL, it does, else it tries to match known logical ValueSet.url values.';
tokenQualifiers.push(new Object()); tokenQualifiers.push({});
tokenQualifiers[6].name='Not-in'; tokenQualifiers[6].name='Not-in';
tokenQualifiers[6].value=':not-in'; tokenQualifiers[6].value=':not-in';
tokenQualifiers[6].description='The search parameter is a URI (relative or absolute) that identifies a value set, and the search parameter tests whether the coding is not in the specified value set.'; tokenQualifiers[6].description='The search parameter is a URI (relative or absolute) that identifies a value set, and the search parameter tests whether the coding is not in the specified value set.';
@ -117,8 +117,8 @@ function addSearchControls(theConformance, theSearchParamType, theSearchParamNam
tokenQualifierInput.val(value); tokenQualifierInput.val(value);
tokenQualifierLabel.text(name); tokenQualifierLabel.text(name);
} }
}; }
var tokenQualifierLabel = $('<span>' + tokenQualifiers[0].name + '</span>'); var tokenQualifierLabel = $('<span>' + tokenQualifiers[0].name + '</span>');
var tokenQualifierDropdown = $('<ul />', {'class':'dropdown-menu', role:'menu'}); var tokenQualifierDropdown = $('<ul />', {'class':'dropdown-menu', role:'menu'});
for (var i = 0; i < tokenQualifiers.length; i++) { for (var i = 0; i < tokenQualifiers.length; i++) {
var qualName = tokenQualifiers[i].name; var qualName = tokenQualifiers[i].name;
@ -152,13 +152,13 @@ function addSearchControls(theConformance, theSearchParamType, theSearchParamNam
) )
); );
} else if (theSearchParamType == 'string') { } else if (theSearchParamType === 'string') {
var placeholderText = 'value'; var placeholderText = 'value';
var qualifiers = new Array(); var qualifiers = [];
qualifiers.push(new Object()); qualifiers.push({});
qualifiers[0].name='Matches'; qualifiers[0].name='Matches';
qualifiers[0].value=''; qualifiers[0].value='';
qualifiers.push(new Object()); qualifiers.push({});
qualifiers[1].name='Exactly'; qualifiers[1].name='Exactly';
qualifiers[1].value=':exact'; qualifiers[1].value=':exact';
@ -167,7 +167,6 @@ function addSearchControls(theConformance, theSearchParamType, theSearchParamNam
qualifierInput qualifierInput
); );
var matchesLabel = $('<span>' + qualifiers[0].name + '</span>'); var matchesLabel = $('<span>' + qualifiers[0].name + '</span>');
var qualifierDropdown = $('<ul />', {'class':'dropdown-menu', role:'menu'}); var qualifierDropdown = $('<ul />', {'class':'dropdown-menu', role:'menu'});
@ -176,9 +175,8 @@ function addSearchControls(theConformance, theSearchParamType, theSearchParamNam
qualifierInput.val(value); qualifierInput.val(value);
matchesLabel.text(name); matchesLabel.text(name);
} }
}; }
for (var i = 0; i < qualifiers.length; i++) {
for (var i = 0; i < qualifiers.length; i++) {
var nextLink = $('<a>' + qualifiers[i].name+'</a>'); var nextLink = $('<a>' + qualifiers[i].name+'</a>');
var qualName = qualifiers[i].name; var qualName = qualifiers[i].name;
var nextValue = qualifiers[i].value; var nextValue = qualifiers[i].value;
@ -241,7 +239,7 @@ function addSearchControls(theConformance, theSearchParamType, theSearchParamNam
var select = $('<select/>', {/*style:'margin-left:30px;'*/}); var select = $('<select/>', {/*style:'margin-left:30px;'*/});
var newContainerRowNum = theContainerRowNum + "-0"; var newContainerRowNum = theContainerRowNum + "-0";
var newContainer = $('<div />', { id: 'search-param-rowopts-' + newContainerRowNum }) var newContainer = $('<div />', { id: 'search-param-rowopts-' + newContainerRowNum });
$('#search-param-rowopts-' + theContainerRowNum).append( $('#search-param-rowopts-' + theContainerRowNum).append(
$('<br clear="all" />'), $('<br clear="all" />'),
@ -255,14 +253,14 @@ function addSearchControls(theConformance, theSearchParamType, theSearchParamNam
newContainer newContainer
); );
var params = new Array(); var params = [];
{ {
var param = new Object(); var param = {};
param.type = 'id'; param.type = 'id';
param.chain = ''; param.chain = '';
param.name = theSearchParamName; param.name = theSearchParamName;
param.documentation = 'The resource identity'; param.documentation = 'The resource identity';
param.target = new Array(); param.target = [];
params[theSearchParamName] = param; params[theSearchParamName] = param;
select.append( select.append(
$('<option />', { value: theSearchParamName }).text(param.name + ' - ' + param.documentation) $('<option />', { value: theSearchParamName }).text(param.name + ' - ' + param.documentation)
@ -299,7 +297,63 @@ function addSearchControls(theConformance, theSearchParamType, theSearchParamNam
handleSearchParamTypeChange(select, params, newContainerRowNum, theRowNum); handleSearchParamTypeChange(select, params, newContainerRowNum, theRowNum);
select.change(function(){ handleSearchParamTypeChange(select, params, newContainerRowNum, theRowNum); }); select.change(function(){ handleSearchParamTypeChange(select, params, newContainerRowNum, theRowNum); });
addNameAndType = false; addNameAndType = false;
}
} else if (theSearchParamType == 'uri') {
var placeholderText = 'value';
var qualifiers = [];
qualifiers.push({});
qualifiers[0].name = 'Equals';
qualifiers[0].value = '';
qualifiers.push({});
qualifiers[1].name = 'Above';
qualifiers[1].value = ':above';
qualifiers.push({});
qualifiers[2].name = 'Below';
qualifiers[2].value = ':below';
var qualifierInput = $('<input />', {id: 'param.' + theRowNum + '.qualifier', type: 'hidden'});
$('#search-param-rowopts-' + theContainerRowNum).append(
qualifierInput
);
var matchesLabel = $('<span>' + qualifiers[0].name + '</span>');
var qualifierDropdown = $('<ul />', {'class': 'dropdown-menu', role: 'menu'});
function clickFunction(value, name) {
return function () {
qualifierInput.val(value);
matchesLabel.text(name);
}
}
for (var i = 0; i < qualifiers.length; i++) {
var nextLink = $('<a>' + qualifiers[i].name + '</a>');
var qualName = qualifiers[i].name;
var nextValue = qualifiers[i].value;
qualifierDropdown.append($('<li />').append(nextLink));
nextLink.click(clickFunction(nextValue, qualName));
}
$('#search-param-rowopts-' + theContainerRowNum).append(
$('<div />', {'class': 'col-sm-5 input-group'}).append(
$('<div />', {'class': 'input-group-btn'}).append(
$('<button />', {'class': 'btn btn-default dropdown-toggle', 'data-toggle': 'dropdown'}).append(
matchesLabel,
$('<span class="caret" style="margin-left: 5px;"></span>')
),
qualifierDropdown
),
$('<input />', {
id: 'param.' + theRowNum + '.0',
placeholder: placeholderText,
type: 'text',
'class': 'form-control'
})
)
);
addNameAndType = true;
}
if (addNameAndType) { if (addNameAndType) {
$('#search-param-rowopts-' + theContainerRowNum).append( $('#search-param-rowopts-' + theContainerRowNum).append(
@ -314,12 +368,13 @@ function addSearchControlDate(theSearchParamName, theContainerRowNum, theRowNum,
var inputId0 = theRowNum + '.' + (theLower ? 0 : 2); var inputId0 = theRowNum + '.' + (theLower ? 0 : 2);
var inputId1 = theRowNum + '.' + (theLower ? 1 : 3); var inputId1 = theRowNum + '.' + (theLower ? 1 : 3);
var qualifier = $('<input />', {type:'hidden', id:'param.'+inputId0, id:'param.'+inputId0}); var qualifier = $('<input />', {type:'hidden', id:'param.'+inputId0});
var input;
if (/date$/.test(theSearchParamName)) { if (/date$/.test(theSearchParamName)) {
var input = $('<div />', { 'class':'input-group date', 'data-date-format':'YYYY-MM-DD' }); input = $('<div />', { 'class':'input-group date', 'data-date-format':'YYYY-MM-DD' });
} else { } else {
var input = $('<div />', { 'class':'input-group date', 'data-date-format':'YYYY-MM-DDTHH:mm:ss' }); input = $('<div />', { 'class':'input-group date', 'data-date-format':'YYYY-MM-DDTHH:mm:ss' });
} }
var qualifierDiv = $('<div />'); var qualifierDiv = $('<div />');
@ -426,7 +481,7 @@ function addSearchControlQuantity(theSearchParamName, theContainerRowNum, theRow
function handleSearchParamTypeChange(select, params, theContainerRowNum, theParamRowNum) { function handleSearchParamTypeChange(select, params, theContainerRowNum, theParamRowNum) {
var oldVal = select.prevVal; var oldVal = select.prevVal;
var newVal = select.val(); var newVal = select.val();
if (oldVal == newVal) { if (oldVal === newVal) {
return; return;
} }
$('#search-param-rowopts-' + theContainerRowNum).empty(); $('#search-param-rowopts-' + theContainerRowNum).empty();
@ -448,10 +503,9 @@ function handleSearchParamTypeChange(select, params, theContainerRowNum, thePara
function readFromEntriesTable(source, type, id, vid) { function readFromEntriesTable(source, type, id, vid) {
var btn = $(source); var btn = $(source);
btn.button('loading'); btn.button('loading');
var resId = source.resourceid;
btn.append($('<input />', { type: 'hidden', name: 'id', value: id })); btn.append($('<input />', { type: 'hidden', name: 'id', value: id }));
var resVid = source.resourcevid; var resVid = source.resourcevid;
if (resVid != '') { if (resVid !== '') {
btn.append($('<input />', { type: 'hidden', name: 'vid', value: vid })); btn.append($('<input />', { type: 'hidden', name: 'vid', value: vid }));
} }
setResource(btn, type); setResource(btn, type);
@ -465,10 +519,9 @@ function readFromEntriesTable(source, type, id, vid) {
function updateFromEntriesTable(source, type, id, vid) { function updateFromEntriesTable(source, type, id, vid) {
var btn = $(source); var btn = $(source);
btn.button('loading'); btn.button('loading');
var resId = source.resourceid;
btn.append($('<input />', { type: 'hidden', name: 'updateId', value: id })); btn.append($('<input />', { type: 'hidden', name: 'updateId', value: id }));
var resVid = source.resourcevid; var resVid = source.resourcevid;
if (resVid != '') { if (resVid !== '') {
btn.append($('<input />', { type: 'hidden', name: 'updateVid', value: vid })); btn.append($('<input />', { type: 'hidden', name: 'updateVid', value: vid }));
} }
setResource(btn, type); setResource(btn, type);
@ -488,7 +541,7 @@ function updateURLParameter(url, param, paramVal){
if (additionalURL) { if (additionalURL) {
tempArray = additionalURL.split("&"); tempArray = additionalURL.split("&");
for (i=0; i<tempArray.length; i++){ for (i=0; i<tempArray.length; i++){
if(tempArray[i].split('=')[0] != param){ if(tempArray[i].split('=')[0] !== param){
newAdditionalURL += temp + tempArray[i]; newAdditionalURL += temp + tempArray[i];
temp = "&"; temp = "&";
} }
@ -501,8 +554,9 @@ function updateURLParameter(url, param, paramVal){
function selectServer(serverId) { function selectServer(serverId) {
$('#serverSelectorFhirIcon').removeClass(); let $serverSelectorFhirIcon = $('#serverSelectorFhirIcon');
$('#serverSelectorFhirIcon').addClass('fa fa-spinner fa-spin'); $serverSelectorFhirIcon.removeClass();
$serverSelectorFhirIcon.addClass('fa fa-spinner fa-spin');
$('#serverSelectorName').text("Loading..."); $('#serverSelectorName').text("Loading...");
$('#serverId').val(serverId); $('#serverId').val(serverId);
$("#outerForm").attr("action", "home").submit(); $("#outerForm").attr("action", "home").submit();
@ -511,7 +565,6 @@ function selectServer(serverId) {
function setResource(target, resourceName) { function setResource(target, resourceName) {
var resource = $('#resource'); var resource = $('#resource');
if (resourceName != null) { if (resourceName != null) {
var input = $('#resource');
if (resource.length) { if (resource.length) {
resource.val(resourceName); resource.val(resourceName);
} else { } else {
@ -527,7 +580,7 @@ function setResource(target, resourceName) {
function updateSort(value) { function updateSort(value) {
$('#sort_by').val(value); $('#sort_by').val(value);
if (value == '') { if (value === '') {
$('#search_sort_button').text('Default Sort'); $('#search_sort_button').text('Default Sort');
} else { } else {
$('#search_sort_button').text(value); $('#search_sort_button').text(value);
@ -536,7 +589,7 @@ function updateSort(value) {
function updateSortDirection(value) { function updateSortDirection(value) {
$('#sort_direction').val(value); $('#sort_direction').val(value);
if (value == '') { if (value === '') {
$('#search_sort_direction_button').text('Default'); $('#search_sort_direction_button').text('Default');
} else { } else {
$('#search_sort_direction_button').text(value); $('#search_sort_direction_button').text(value);

View File

@ -473,6 +473,9 @@
The @ProvidesResources annotation has been removed from HAPI FHIR, as it was not documented The @ProvidesResources annotation has been removed from HAPI FHIR, as it was not documented
and did not do anything useful. Please get in touch if this causes any issues. and did not do anything useful. Please get in touch if this causes any issues.
</action> </action>
<action type="fix">
Search parameters of type URI did not work in the hapi-fhir-testpage-overlay. This has been corrected.
</action>
</release> </release>
<release version="4.0.3" date="2019-09-03" description="Igloo (Point Release)"> <release version="4.0.3" date="2019-09-03" description="Igloo (Point Release)">
<action type="fix"> <action type="fix">