Merge branch 'master' of https://github.com/jamesagnew/hapi-fhir
This commit is contained in:
commit
208d13ec5e
|
@ -48,4 +48,8 @@ public interface IFluentPath {
|
||||||
<T extends IBase> Optional<T> evaluateFirst(IBase theInput, String thePath, Class<T> theReturnType);
|
<T extends IBase> Optional<T> evaluateFirst(IBase theInput, String thePath, Class<T> theReturnType);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the expression and throws an exception if it can not parse correctly
|
||||||
|
*/
|
||||||
|
void parse(String theExpression) throws Exception;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1599,6 +1599,33 @@ public enum Pointcut {
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* THIS IS AN EXPERIMENTAL HOOK AND MAY BE REMOVED OR CHANGED WITHOUT WARNING.
|
||||||
|
*
|
||||||
|
* Note that this is a performance tracing hook. Use with caution in production
|
||||||
|
* systems, since calling it may (or may not) carry a cost.
|
||||||
|
* <p>
|
||||||
|
* This hook is invoked when a search has found an individual ID.
|
||||||
|
* </p>
|
||||||
|
* Hooks may accept the following parameters:
|
||||||
|
* <ul>
|
||||||
|
* <li>
|
||||||
|
* java.lang.Integer - The query ID
|
||||||
|
* </li>
|
||||||
|
* <li>
|
||||||
|
* java.lang.Object - The ID
|
||||||
|
* </li>
|
||||||
|
* </ul>
|
||||||
|
* <p>
|
||||||
|
* Hooks should return <code>void</code>.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
JPA_PERFTRACE_SEARCH_FOUND_ID(void.class,
|
||||||
|
"java.lang.Integer",
|
||||||
|
"java.lang.Object"
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note that this is a performance tracing hook. Use with caution in production
|
* Note that this is a performance tracing hook. Use with caution in production
|
||||||
* systems, since calling it may (or may not) carry a cost.
|
* systems, since calling it may (or may not) carry a cost.
|
||||||
|
|
|
@ -109,6 +109,8 @@ ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor.failedToExtractPa
|
||||||
ca.uhn.fhir.jpa.dao.SearchBuilder.invalidQuantityPrefix=Unable to handle quantity prefix "{0}" for value: {1}
|
ca.uhn.fhir.jpa.dao.SearchBuilder.invalidQuantityPrefix=Unable to handle quantity prefix "{0}" for value: {1}
|
||||||
ca.uhn.fhir.jpa.dao.SearchBuilder.invalidNumberPrefix=Unable to handle number prefix "{0}" for value: {1}
|
ca.uhn.fhir.jpa.dao.SearchBuilder.invalidNumberPrefix=Unable to handle number prefix "{0}" for value: {1}
|
||||||
ca.uhn.fhir.jpa.dao.SearchBuilder.sourceParamDisabled=The _source parameter is disabled on this server
|
ca.uhn.fhir.jpa.dao.SearchBuilder.sourceParamDisabled=The _source parameter is disabled on this server
|
||||||
|
ca.uhn.fhir.jpa.dao.SearchBuilder.invalidCodeMissingSystem=Invalid token specified for parameter {0} - No system specified: {1}|{2}
|
||||||
|
ca.uhn.fhir.jpa.dao.SearchBuilder.invalidCodeMissingCode=Invalid token specified for parameter {0} - No code specified: {1}|{2}
|
||||||
|
|
||||||
ca.uhn.fhir.jpa.dao.dstu3.FhirResourceDaoConceptMapDstu3.matchesFound=Matches found!
|
ca.uhn.fhir.jpa.dao.dstu3.FhirResourceDaoConceptMapDstu3.matchesFound=Matches found!
|
||||||
ca.uhn.fhir.jpa.dao.dstu3.FhirResourceDaoConceptMapDstu3.noMatchesFound=No matches found!
|
ca.uhn.fhir.jpa.dao.dstu3.FhirResourceDaoConceptMapDstu3.noMatchesFound=No matches found!
|
||||||
|
|
|
@ -1857,9 +1857,11 @@ public class SearchBuilder implements ISearchBuilder {
|
||||||
codes.addAll(myTerminologySvc.expandValueSet(code));
|
codes.addAll(myTerminologySvc.expandValueSet(code));
|
||||||
} else if (modifier == TokenParamModifier.ABOVE) {
|
} else if (modifier == TokenParamModifier.ABOVE) {
|
||||||
system = determineSystemIfMissing(theParamName, code, system);
|
system = determineSystemIfMissing(theParamName, code, system);
|
||||||
|
validateHaveSystemAndCodeForToken(theParamName, code, system);
|
||||||
codes.addAll(myTerminologySvc.findCodesAbove(system, code));
|
codes.addAll(myTerminologySvc.findCodesAbove(system, code));
|
||||||
} else if (modifier == TokenParamModifier.BELOW) {
|
} else if (modifier == TokenParamModifier.BELOW) {
|
||||||
system = determineSystemIfMissing(theParamName, code, system);
|
system = determineSystemIfMissing(theParamName, code, system);
|
||||||
|
validateHaveSystemAndCodeForToken(theParamName, code, system);
|
||||||
codes.addAll(myTerminologySvc.findCodesBelow(system, code));
|
codes.addAll(myTerminologySvc.findCodesBelow(system, code));
|
||||||
} else {
|
} else {
|
||||||
codes.add(new VersionIndependentConcept(system, code));
|
codes.add(new VersionIndependentConcept(system, code));
|
||||||
|
@ -1902,6 +1904,19 @@ public class SearchBuilder implements ISearchBuilder {
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateHaveSystemAndCodeForToken(String theParamName, String theCode, String theSystem) {
|
||||||
|
String systemDesc = defaultIfBlank(theSystem, "(missing)");
|
||||||
|
String codeDesc = defaultIfBlank(theCode, "(missing)");
|
||||||
|
if (isBlank(theCode)) {
|
||||||
|
String msg = myContext.getLocalizer().getMessage(SearchBuilder.class, "invalidCodeMissingSystem", theParamName, systemDesc, codeDesc);
|
||||||
|
throw new InvalidRequestException(msg);
|
||||||
|
}
|
||||||
|
if (isBlank(theSystem)) {
|
||||||
|
String msg = myContext.getLocalizer().getMessage(SearchBuilder.class, "invalidCodeMissingCode", theParamName, systemDesc, codeDesc);
|
||||||
|
throw new InvalidRequestException(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Predicate addPredicateToken(String theResourceName, String theParamName, CriteriaBuilder theBuilder, From<?, ResourceIndexedSearchParamToken> theFrom, List<VersionIndependentConcept> theTokens, TokenParamModifier theModifier, TokenModeEnum theTokenMode) {
|
private Predicate addPredicateToken(String theResourceName, String theParamName, CriteriaBuilder theBuilder, From<?, ResourceIndexedSearchParamToken> theFrom, List<VersionIndependentConcept> theTokens, TokenParamModifier theModifier, TokenModeEnum theTokenMode) {
|
||||||
if (myDontUseHashesForSearch) {
|
if (myDontUseHashesForSearch) {
|
||||||
final Path<String> systemExpression = theFrom.get("mySystem");
|
final Path<String> systemExpression = theFrom.get("mySystem");
|
||||||
|
@ -2056,6 +2071,7 @@ public class SearchBuilder implements ISearchBuilder {
|
||||||
outerQuery.multiselect(myBuilder.countDistinct(myResourceTableRoot));
|
outerQuery.multiselect(myBuilder.countDistinct(myResourceTableRoot));
|
||||||
} else {
|
} else {
|
||||||
outerQuery.multiselect(myResourceTableRoot.get("myId").as(Long.class));
|
outerQuery.multiselect(myResourceTableRoot.get("myId").as(Long.class));
|
||||||
|
outerQuery.distinct(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3125,6 +3141,8 @@ public class SearchBuilder implements ISearchBuilder {
|
||||||
|
|
||||||
private final SearchRuntimeDetails mySearchRuntimeDetails;
|
private final SearchRuntimeDetails mySearchRuntimeDetails;
|
||||||
private final RequestDetails myRequest;
|
private final RequestDetails myRequest;
|
||||||
|
private final boolean myHaveRawSqlHooks;
|
||||||
|
private final boolean myHavePerftraceFoundIdHook;
|
||||||
private boolean myFirst = true;
|
private boolean myFirst = true;
|
||||||
private IncludesIterator myIncludesIterator;
|
private IncludesIterator myIncludesIterator;
|
||||||
private Long myNext;
|
private Long myNext;
|
||||||
|
@ -3143,13 +3161,16 @@ public class SearchBuilder implements ISearchBuilder {
|
||||||
if (myParams.getEverythingMode() != null) {
|
if (myParams.getEverythingMode() != null) {
|
||||||
myStillNeedToFetchIncludes = true;
|
myStillNeedToFetchIncludes = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
myHavePerftraceFoundIdHook =JpaInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_SEARCH_FOUND_ID, myInterceptorBroadcaster, myRequest);
|
||||||
|
myHaveRawSqlHooks = JpaInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_RAW_SQL, myInterceptorBroadcaster, myRequest);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fetchNext() {
|
private void fetchNext() {
|
||||||
|
|
||||||
boolean haveRawSqlHooks = JpaInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_RAW_SQL, myInterceptorBroadcaster, myRequest);
|
|
||||||
try {
|
try {
|
||||||
if (haveRawSqlHooks) {
|
if (myHaveRawSqlHooks) {
|
||||||
CurrentThreadCaptureQueriesListener.startCapturing();
|
CurrentThreadCaptureQueriesListener.startCapturing();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3190,6 +3211,13 @@ public class SearchBuilder implements ISearchBuilder {
|
||||||
if (myNext == null) {
|
if (myNext == null) {
|
||||||
while (myResultsIterator.hasNext()) {
|
while (myResultsIterator.hasNext()) {
|
||||||
Long next = myResultsIterator.next();
|
Long next = myResultsIterator.next();
|
||||||
|
if (myHavePerftraceFoundIdHook) {
|
||||||
|
HookParams params = new HookParams()
|
||||||
|
.add(Integer.class, System.identityHashCode(this))
|
||||||
|
.add(Object.class, next);
|
||||||
|
JpaInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_SEARCH_FOUND_ID, params);
|
||||||
|
}
|
||||||
|
|
||||||
if (next != null) {
|
if (next != null) {
|
||||||
if (myPidSet.add(next)) {
|
if (myPidSet.add(next)) {
|
||||||
myNext = next;
|
myNext = next;
|
||||||
|
@ -3228,7 +3256,7 @@ public class SearchBuilder implements ISearchBuilder {
|
||||||
mySearchRuntimeDetails.setFoundMatchesCount(myPidSet.size());
|
mySearchRuntimeDetails.setFoundMatchesCount(myPidSet.size());
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
if (haveRawSqlHooks) {
|
if (myHaveRawSqlHooks) {
|
||||||
SqlQueryList capturedQueries = CurrentThreadCaptureQueriesListener.getCurrentQueueAndStopCapturing();
|
SqlQueryList capturedQueries = CurrentThreadCaptureQueriesListener.getCurrentQueueAndStopCapturing();
|
||||||
HookParams params = new HookParams()
|
HookParams params = new HookParams()
|
||||||
.add(RequestDetails.class, myRequest)
|
.add(RequestDetails.class, myRequest)
|
||||||
|
|
|
@ -145,10 +145,9 @@ public class FhirResourceDaoSearchParameterR4 extends BaseHapiFhirResourceDao<Se
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
FHIRPathEngine fhirPathEngine = new FHIRPathEngine(new HapiWorkerContext(theContext, VALIDATION_SUPPORT));
|
|
||||||
try {
|
try {
|
||||||
fhirPathEngine.parse(theExpression);
|
theContext.newFluentPath().parse(theExpression);
|
||||||
} catch (FHIRLexer.FHIRLexerException e) {
|
} catch (Exception e) {
|
||||||
throw new UnprocessableEntityException("Invalid SearchParameter.expression value \"" + theExpression + "\": " + e.getMessage());
|
throw new UnprocessableEntityException("Invalid SearchParameter.expression value \"" + theExpression + "\": " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
package ca.uhn.fhir.jpa.provider;
|
|
||||||
|
|
||||||
/*-
|
|
||||||
* #%L
|
|
||||||
* HAPI FHIR JPA Server
|
|
||||||
* %%
|
|
||||||
* Copyright (C) 2014 - 2019 University Health Network
|
|
||||||
* %%
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
* #L%
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use ca.uhn.fhir.jpa.binstore.BinaryAccessProvider instead
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public class BinaryAccessProvider extends ca.uhn.fhir.jpa.binstore.BinaryAccessProvider {
|
|
||||||
|
|
||||||
// FIXME: JA delete before 4.0.0
|
|
||||||
|
|
||||||
}
|
|
|
@ -233,6 +233,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
|
||||||
ourLog.trace("Going to try to start next search");
|
ourLog.trace("Going to try to start next search");
|
||||||
Optional<Search> newSearch = mySearchCacheSvc.tryToMarkSearchAsInProgress(search);
|
Optional<Search> newSearch = mySearchCacheSvc.tryToMarkSearchAsInProgress(search);
|
||||||
if (newSearch.isPresent()) {
|
if (newSearch.isPresent()) {
|
||||||
|
ourLog.trace("Launching new search");
|
||||||
search = newSearch.get();
|
search = newSearch.get();
|
||||||
String resourceType = search.getResourceType();
|
String resourceType = search.getResourceType();
|
||||||
SearchParameterMap params = search.getSearchParameterMap().orElseThrow(() -> new IllegalStateException("No map in PASSCOMPLET search"));
|
SearchParameterMap params = search.getSearchParameterMap().orElseThrow(() -> new IllegalStateException("No map in PASSCOMPLET search"));
|
||||||
|
@ -1113,7 +1114,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
|
||||||
throw newResourceGoneException(getSearch().getUuid());
|
throw newResourceGoneException(getSearch().getUuid());
|
||||||
}
|
}
|
||||||
|
|
||||||
ourLog.debug("Have {} previously added IDs in search: {}", previouslyAddedResourcePids.size(), getSearch().getUuid());
|
ourLog.trace("Have {} previously added IDs in search: {}", previouslyAddedResourcePids.size(), getSearch().getUuid());
|
||||||
setPreviouslyAddedResourcePids(previouslyAddedResourcePids);
|
setPreviouslyAddedResourcePids(previouslyAddedResourcePids);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,10 +2,15 @@ package ca.uhn.fhir.jpa.dao.r4;
|
||||||
|
|
||||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||||
import ca.uhn.fhir.jpa.dao.DaoConfig;
|
import ca.uhn.fhir.jpa.dao.DaoConfig;
|
||||||
import ca.uhn.fhir.jpa.dao.data.ISearchDao;
|
|
||||||
import ca.uhn.fhir.jpa.dao.data.ISearchResultDao;
|
|
||||||
import ca.uhn.fhir.jpa.entity.Search;
|
import ca.uhn.fhir.jpa.entity.Search;
|
||||||
import ca.uhn.fhir.jpa.model.entity.*;
|
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate;
|
||||||
|
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber;
|
||||||
|
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity;
|
||||||
|
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
|
||||||
|
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
|
||||||
|
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri;
|
||||||
|
import ca.uhn.fhir.jpa.model.entity.ResourceLink;
|
||||||
|
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||||
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
|
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
|
||||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum;
|
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum;
|
||||||
|
@ -34,7 +39,11 @@ import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender;
|
||||||
import org.hl7.fhir.r4.model.Observation.ObservationStatus;
|
import org.hl7.fhir.r4.model.Observation.ObservationStatus;
|
||||||
import org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType;
|
import org.hl7.fhir.r4.model.Subscription.SubscriptionChannelType;
|
||||||
import org.hl7.fhir.r4.model.Subscription.SubscriptionStatus;
|
import org.hl7.fhir.r4.model.Subscription.SubscriptionStatus;
|
||||||
import org.junit.*;
|
import org.junit.After;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Ignore;
|
||||||
|
import org.junit.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.transaction.TransactionStatus;
|
import org.springframework.transaction.TransactionStatus;
|
||||||
import org.springframework.transaction.support.TransactionCallback;
|
import org.springframework.transaction.support.TransactionCallback;
|
||||||
|
@ -45,11 +54,29 @@ import javax.servlet.http.HttpServletRequest;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.*;
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.*;
|
import static org.hamcrest.Matchers.contains;
|
||||||
import static org.junit.Assert.*;
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.hamcrest.Matchers.empty;
|
||||||
|
import static org.hamcrest.Matchers.endsWith;
|
||||||
|
import static org.hamcrest.Matchers.hasItem;
|
||||||
|
import static org.hamcrest.Matchers.hasItems;
|
||||||
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
|
import static org.hamcrest.Matchers.not;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
@SuppressWarnings({"unchecked", "Duplicates"})
|
@SuppressWarnings({"unchecked", "Duplicates"})
|
||||||
|
@ -1978,6 +2005,52 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSearchOnCodesWithBelow() {
|
||||||
|
myFhirCtx.setParserErrorHandler(new StrictErrorHandler());
|
||||||
|
|
||||||
|
CodeSystem cs = new CodeSystem();
|
||||||
|
cs.setUrl("http://foo");
|
||||||
|
cs.setContent(CodeSystem.CodeSystemContentMode.COMPLETE);
|
||||||
|
cs.addConcept().setCode("111-1")
|
||||||
|
.addConcept().setCode("111-2");
|
||||||
|
cs.addConcept().setCode("222-1")
|
||||||
|
.addConcept().setCode("222-2");
|
||||||
|
myCodeSystemDao.create(cs);
|
||||||
|
|
||||||
|
Observation obs1 = new Observation();
|
||||||
|
obs1.getCode().addCoding().setSystem("http://foo").setCode("111-1");
|
||||||
|
String id1 = myObservationDao.create(obs1).getId().toUnqualifiedVersionless().getValue();
|
||||||
|
|
||||||
|
Observation obs2 = new Observation();
|
||||||
|
obs2.getCode().addCoding().setSystem("http://foo").setCode("111-2");
|
||||||
|
String id2 = myObservationDao.create(obs2).getId().toUnqualifiedVersionless().getValue();
|
||||||
|
|
||||||
|
|
||||||
|
IBundleProvider result;
|
||||||
|
|
||||||
|
result = myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_CODE, new TokenParam("http://foo", "111-1")));
|
||||||
|
assertThat(toUnqualifiedVersionlessIds(result).toString(), toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(id1));
|
||||||
|
|
||||||
|
result = myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_CODE, new TokenParam("http://foo", "111-1").setModifier(TokenParamModifier.BELOW)));
|
||||||
|
assertThat(toUnqualifiedVersionlessIds(result).toString(), toUnqualifiedVersionlessIdValues(result), containsInAnyOrder(id1, id2));
|
||||||
|
|
||||||
|
try {
|
||||||
|
myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_CODE, new TokenParam(null, "111-1").setModifier(TokenParamModifier.BELOW)));
|
||||||
|
fail();
|
||||||
|
} catch (InvalidRequestException e) {
|
||||||
|
assertEquals("Invalid token specified for parameter code - No code specified: (missing)|111-1", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
myObservationDao.search(new SearchParameterMap().setLoadSynchronous(true).add(Observation.SP_CODE, new TokenParam("111-1", null).setModifier(TokenParamModifier.BELOW)));
|
||||||
|
fail();
|
||||||
|
} catch (InvalidRequestException e) {
|
||||||
|
assertEquals("Invalid token specified for parameter code - No system specified: 111-1|(missing)", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSearchParamChangesType() {
|
public void testSearchParamChangesType() {
|
||||||
String name = "testSearchParamChangesType";
|
String name = "testSearchParamChangesType";
|
||||||
|
|
|
@ -1,19 +1,26 @@
|
||||||
package ca.uhn.fhir.jpa.provider.r4;
|
package ca.uhn.fhir.jpa.provider.r4;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.interceptor.api.Hook;
|
||||||
|
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||||
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
|
import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao;
|
||||||
|
import ca.uhn.fhir.jpa.dao.DaoConfig;
|
||||||
import ca.uhn.fhir.jpa.entity.ResourceReindexJobEntity;
|
import ca.uhn.fhir.jpa.entity.ResourceReindexJobEntity;
|
||||||
|
import ca.uhn.fhir.jpa.entity.Search;
|
||||||
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
|
||||||
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
|
||||||
|
import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
|
||||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||||
import ca.uhn.fhir.rest.api.Constants;
|
import ca.uhn.fhir.rest.api.Constants;
|
||||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||||
import ca.uhn.fhir.rest.gclient.ReferenceClientParam;
|
import ca.uhn.fhir.rest.gclient.ReferenceClientParam;
|
||||||
import ca.uhn.fhir.rest.gclient.TokenClientParam;
|
import ca.uhn.fhir.rest.gclient.TokenClientParam;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||||
|
import ca.uhn.fhir.util.BundleUtil;
|
||||||
import ca.uhn.fhir.util.TestUtil;
|
import ca.uhn.fhir.util.TestUtil;
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
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.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
import org.hl7.fhir.instance.model.api.IIdType;
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
import org.hl7.fhir.r4.model.*;
|
import org.hl7.fhir.r4.model.*;
|
||||||
import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestComponent;
|
import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestComponent;
|
||||||
|
@ -30,25 +37,32 @@ import org.springframework.transaction.TransactionStatus;
|
||||||
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
|
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
|
||||||
import org.springframework.transaction.support.TransactionTemplate;
|
import org.springframework.transaction.support.TransactionTemplate;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.contains;
|
import static org.hamcrest.Matchers.contains;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProviderR4Test {
|
public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProviderR4Test {
|
||||||
|
|
||||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceProviderCustomSearchParamR4Test.class);
|
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResourceProviderCustomSearchParamR4Test.class);
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@After
|
@After
|
||||||
public void after() throws Exception {
|
public void after() throws Exception {
|
||||||
super.after();
|
super.after();
|
||||||
|
|
||||||
myModelConfig.setDefaultSearchParamsCanBeOverridden(new ModelConfig().isDefaultSearchParamsCanBeOverridden());
|
myModelConfig.setDefaultSearchParamsCanBeOverridden(new ModelConfig().isDefaultSearchParamsCanBeOverridden());
|
||||||
|
myDaoConfig.setAllowContainsSearches(new DaoConfig().isAllowContainsSearches());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -66,7 +80,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, CapabilityStatementRestResourceSearchParamComponent> extractSearchParams(CapabilityStatement conformance, String resType) {
|
private Map<String, CapabilityStatementRestResourceSearchParamComponent> extractSearchParams(CapabilityStatement conformance, String resType) {
|
||||||
Map<String, CapabilityStatementRestResourceSearchParamComponent> map = new HashMap<String, CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent>();
|
Map<String, CapabilityStatementRestResourceSearchParamComponent> map = new HashMap<>();
|
||||||
for (CapabilityStatementRestComponent nextRest : conformance.getRest()) {
|
for (CapabilityStatementRestComponent nextRest : conformance.getRest()) {
|
||||||
for (CapabilityStatementRestResourceComponent nextResource : nextRest.getResource()) {
|
for (CapabilityStatementRestResourceComponent nextResource : nextRest.getResource()) {
|
||||||
if (!resType.equals(nextResource.getType())) {
|
if (!resType.equals(nextResource.getType())) {
|
||||||
|
@ -81,7 +95,7 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void saveCreateSearchParamInvalidWithMissingStatus() throws IOException {
|
public void saveCreateSearchParamInvalidWithMissingStatus() {
|
||||||
SearchParameter sp = new SearchParameter();
|
SearchParameter sp = new SearchParameter();
|
||||||
sp.setCode("foo");
|
sp.setCode("foo");
|
||||||
sp.setExpression("Patient.gender");
|
sp.setExpression("Patient.gender");
|
||||||
|
@ -431,6 +445,127 @@ public class ResourceProviderCustomSearchParamR4Test extends BaseResourceProvide
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See #1300
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testCustomParameterMatchingManyValues() {
|
||||||
|
|
||||||
|
List<String> found = new ArrayList<>();
|
||||||
|
|
||||||
|
class Interceptor {
|
||||||
|
@Hook(Pointcut.JPA_PERFTRACE_SEARCH_FOUND_ID)
|
||||||
|
public void foundId(Integer theSearchId, Object theId) {
|
||||||
|
found.add(theSearchId + "/" + theId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Interceptor interceptor = new Interceptor();
|
||||||
|
myInterceptorRegistry.registerInterceptor(interceptor);
|
||||||
|
try {
|
||||||
|
myDaoConfig.setAllowContainsSearches(true);
|
||||||
|
|
||||||
|
// Add a custom search parameter
|
||||||
|
SearchParameter fooSp = new SearchParameter();
|
||||||
|
fooSp.addBase("Questionnaire");
|
||||||
|
fooSp.setCode("item-text");
|
||||||
|
fooSp.setName("item-text");
|
||||||
|
fooSp.setType(Enumerations.SearchParamType.STRING);
|
||||||
|
fooSp.setTitle("FOO SP");
|
||||||
|
fooSp.setExpression("Questionnaire.item.text | Questionnaire.item.item.text | Questionnaire.item.item.item.text");
|
||||||
|
fooSp.setXpathUsage(org.hl7.fhir.r4.model.SearchParameter.XPathUsageType.NORMAL);
|
||||||
|
fooSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE);
|
||||||
|
mySearchParameterDao.create(fooSp, mySrd);
|
||||||
|
mySearchParamRegistry.forceRefresh();
|
||||||
|
|
||||||
|
int textIndex = 0;
|
||||||
|
List<Long> ids = new ArrayList<>();
|
||||||
|
for (int i = 0; i < 200; i++) {
|
||||||
|
//Lots and lots of matches
|
||||||
|
Questionnaire q = new Questionnaire();
|
||||||
|
q
|
||||||
|
.addItem()
|
||||||
|
.setText("Section " + (textIndex++))
|
||||||
|
.addItem()
|
||||||
|
.setText("Section " + (textIndex++))
|
||||||
|
.addItem()
|
||||||
|
.setText("Section " + (textIndex++));
|
||||||
|
q
|
||||||
|
.addItem()
|
||||||
|
.setText("Section " + (textIndex++))
|
||||||
|
.addItem()
|
||||||
|
.setText("Section " + (textIndex++))
|
||||||
|
.addItem()
|
||||||
|
.setText("Section " + (textIndex++));
|
||||||
|
q
|
||||||
|
.addItem()
|
||||||
|
.setText("Section " + (textIndex++))
|
||||||
|
.addItem()
|
||||||
|
.setText("Section " + (textIndex++))
|
||||||
|
.addItem()
|
||||||
|
.setText("Section " + (textIndex++));
|
||||||
|
q
|
||||||
|
.addItem()
|
||||||
|
.setText("Section " + (textIndex++))
|
||||||
|
.addItem()
|
||||||
|
.setText("Section " + (textIndex++))
|
||||||
|
.addItem()
|
||||||
|
.setText("Section " + (textIndex++));
|
||||||
|
q
|
||||||
|
.addItem()
|
||||||
|
.setText("Section " + (textIndex++))
|
||||||
|
.addItem()
|
||||||
|
.setText("Section " + (textIndex++))
|
||||||
|
.addItem()
|
||||||
|
.setText("Section " + (textIndex++));
|
||||||
|
ids.add(myQuestionnaireDao.create(q).getId().getIdPartAsLong());
|
||||||
|
}
|
||||||
|
|
||||||
|
int foundCount = 0;
|
||||||
|
Bundle bundle = null;
|
||||||
|
List<Long> actualIds = new ArrayList<>();
|
||||||
|
do {
|
||||||
|
|
||||||
|
if (bundle == null) {
|
||||||
|
bundle = ourClient
|
||||||
|
.search()
|
||||||
|
.byUrl(ourServerBase + "/Questionnaire?item-text=Section")
|
||||||
|
.returnBundle(Bundle.class)
|
||||||
|
.execute();
|
||||||
|
} else {
|
||||||
|
bundle = ourClient
|
||||||
|
.loadPage()
|
||||||
|
.next(bundle)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
List<IBaseResource> resources = BundleUtil.toListOfResources(myFhirCtx, bundle);
|
||||||
|
resources.forEach(t -> actualIds.add(t.getIdElement().getIdPartAsLong()));
|
||||||
|
foundCount += resources.size();
|
||||||
|
|
||||||
|
} while (bundle.getLink("next") != null);
|
||||||
|
|
||||||
|
ourLog.info("Found: {}", found);
|
||||||
|
|
||||||
|
runInTransaction(() -> {
|
||||||
|
|
||||||
|
List<Search> searches = mySearchEntityDao.findAll();
|
||||||
|
assertEquals(1, searches.size());
|
||||||
|
Search search = searches.get(0);
|
||||||
|
String message = "\nWanted: " + (ids) + "\n" +
|
||||||
|
"Actual: " + (actualIds) + "\n" +
|
||||||
|
"Found : " + (found) + "\n" +
|
||||||
|
search.toString();
|
||||||
|
assertEquals(message, 200, search.getNumFound());
|
||||||
|
assertEquals(message, 200, search.getTotalCount().intValue());
|
||||||
|
assertEquals(message, SearchStatusEnum.FINISHED, search.getStatus());
|
||||||
|
});
|
||||||
|
|
||||||
|
assertEquals(200, foundCount);
|
||||||
|
} finally {
|
||||||
|
myInterceptorRegistry.unregisterInterceptor(interceptor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@AfterClass
|
@AfterClass
|
||||||
public static void afterClassClearContext() {
|
public static void afterClassClearContext() {
|
||||||
TestUtil.clearAllStaticFieldsForUnitTest();
|
TestUtil.clearAllStaticFieldsForUnitTest();
|
||||||
|
|
|
@ -7,7 +7,13 @@ import ca.uhn.fhir.jpa.dao.dstu3.BaseJpaDstu3Test;
|
||||||
import ca.uhn.fhir.jpa.term.api.ITermLoaderSvc;
|
import ca.uhn.fhir.jpa.term.api.ITermLoaderSvc;
|
||||||
import ca.uhn.fhir.util.TestUtil;
|
import ca.uhn.fhir.util.TestUtil;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import org.hl7.fhir.dstu3.model.*;
|
import org.hl7.fhir.dstu3.model.CodeType;
|
||||||
|
import org.hl7.fhir.dstu3.model.Coding;
|
||||||
|
import org.hl7.fhir.dstu3.model.Parameters;
|
||||||
|
import org.hl7.fhir.dstu3.model.PrimitiveType;
|
||||||
|
import org.hl7.fhir.dstu3.model.StringType;
|
||||||
|
import org.hl7.fhir.dstu3.model.Type;
|
||||||
|
import org.hl7.fhir.dstu3.model.ValueSet;
|
||||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.AfterClass;
|
import org.junit.AfterClass;
|
||||||
|
@ -24,8 +30,13 @@ import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.awaitility.Awaitility.await;
|
import static org.awaitility.Awaitility.await;
|
||||||
import static org.hamcrest.Matchers.*;
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
import static org.junit.Assert.*;
|
import static org.hamcrest.Matchers.empty;
|
||||||
|
import static org.hamcrest.Matchers.greaterThan;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class TerminologyLoaderSvcIntegrationDstu3Test extends BaseJpaDstu3Test {
|
public class TerminologyLoaderSvcIntegrationDstu3Test extends BaseJpaDstu3Test {
|
||||||
|
|
||||||
|
@ -116,9 +127,7 @@ public class TerminologyLoaderSvcIntegrationDstu3Test extends BaseJpaDstu3Test {
|
||||||
|
|
||||||
myTerminologyDeferredStorageSvc.saveDeferred();
|
myTerminologyDeferredStorageSvc.saveDeferred();
|
||||||
|
|
||||||
runInTransaction(() -> {
|
await().until(() -> runInTransaction(() -> myTermConceptMapDao.count()), greaterThan(0L));
|
||||||
await().until(() -> myTermConceptMapDao.count(), greaterThan(0L));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"resourceType": "SearchParameter",
|
||||||
|
"id": "54805",
|
||||||
|
"meta": {
|
||||||
|
"versionId": "1",
|
||||||
|
"lastUpdated": "2019-11-04T18:33:47.918+00:00",
|
||||||
|
"source": "#pt4ERkOO6LGm5ZoA"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"status": "generated",
|
||||||
|
"div": "<div xmlns=\"http://www.w3.org/1999/xhtml\">Search for a questionnaire by item.text nested field - up to 3 levels.</div>"
|
||||||
|
},
|
||||||
|
"url": "https://impact-fhir.mitre.org/r4/SearchParameter/QuestionnaireItemText",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"name": "QuestionnaireItemText",
|
||||||
|
"status": "active",
|
||||||
|
"date": "2019-10-25T15:38:45-04:00",
|
||||||
|
"description": "Search for a questionnaire by item.text nested field - up to 3 levels.",
|
||||||
|
"code": "item-text",
|
||||||
|
"base": [
|
||||||
|
"Questionnaire"
|
||||||
|
],
|
||||||
|
"type": "string",
|
||||||
|
"expression": "Questionnaire.item.text | Questionnaire.item.item.text | Questionnaire.item.item.item.text",
|
||||||
|
"xpathUsage": "normal",
|
||||||
|
"modifier": [
|
||||||
|
"contains"
|
||||||
|
]
|
||||||
|
}
|
|
@ -49,4 +49,9 @@ public class FluentPathDstu3 implements IFluentPath {
|
||||||
return evaluate(theInput, thePath, theReturnType).stream().findFirst();
|
return evaluate(theInput, thePath, theReturnType).stream().findFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void parse(String theExpression) {
|
||||||
|
myEngine.parse(theExpression);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,5 +49,10 @@ public class FluentPathR4 implements IFluentPath {
|
||||||
return evaluate(theInput, thePath, theReturnType).stream().findFirst();
|
return evaluate(theInput, thePath, theReturnType).stream().findFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void parse(String theExpression) {
|
||||||
|
myEngine.parse(theExpression);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,5 +49,10 @@ public class FhirPathR5 implements IFluentPath {
|
||||||
return evaluate(theInput, thePath, theReturnType).stream().findFirst();
|
return evaluate(theInput, thePath, theReturnType).stream().findFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void parse(String theExpression) {
|
||||||
|
myEngine.parse(theExpression);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -501,6 +501,18 @@
|
||||||
A NullPointerException in the XML Parser was fixed when serializing a resource containing an extension
|
A NullPointerException in the XML Parser was fixed when serializing a resource containing an extension
|
||||||
on a primitive datatype that was missing a URL declaration.
|
on a primitive datatype that was missing a URL declaration.
|
||||||
</action>
|
</action>
|
||||||
|
<action type="fix">
|
||||||
|
When using the _filter search parameter in the JPA server with an untyped resource ID, the
|
||||||
|
filter could bring in search results of the wrong type. Thanks to Anthony Sute for the Pull
|
||||||
|
Request and Jens Villadsen for reporting!
|
||||||
|
</action>
|
||||||
|
<action type="fix" issue="1300">
|
||||||
|
In some cases where where a single search parameter matches the same resource many times with
|
||||||
|
different distinct values (e.g. a search by Patient:name where there are hundreds of patients having
|
||||||
|
hundreds of distinct names each) the Search Coordinator would end up in an infinite loop and never
|
||||||
|
return all of the possible results. Thanks to @imranmoezkhan for reporting, and to
|
||||||
|
Tim Shaffer for providing a reproducible test case!
|
||||||
|
</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">
|
||||||
|
|
|
@ -17,9 +17,9 @@
|
||||||
<br/>
|
<br/>
|
||||||
<iframe src="https://ghbtns.com/github-btn.html?user=jamesagnew&repo=hapi-fhir&type=fork&count=true&v=2" frameborder="0" scrolling="0" width="100px" height="20px"></iframe>
|
<iframe src="https://ghbtns.com/github-btn.html?user=jamesagnew&repo=hapi-fhir&type=fork&count=true&v=2" frameborder="0" scrolling="0" width="100px" height="20px"></iframe>
|
||||||
<br/>
|
<br/>
|
||||||
<a href="https://travis-ci.org/jamesagnew/hapi-fhir"><img src="https://travis-ci.org/jamesagnew/hapi-fhir.svg?branch=master" alt="Build Status"/></a>
|
<a href="https://dev.azure.com/jamesagnew214/jamesagnew214/_build/latest?definitionId=1&branchName=master"><img src="https://dev.azure.com/jamesagnew214/jamesagnew214/_apis/build/status/jamesagnew.hapi-fhir?branchName=master" alt="Build Status"/></a>
|
||||||
<br/>
|
<br/>
|
||||||
<a href="https://coveralls.io/r/jamesagnew/hapi-fhir?branch=master"><img src="https://coveralls.io/repos/jamesagnew/hapi-fhir/badge.svg?branch=master" alt="Coverage Status"/></a>
|
<a href="https://codecov.io/gh/jamesagnew/hapi-fhir"><img src="https://codecov.io/gh/jamesagnew/hapi-fhir/branch/master/graph/badge.svg" alt="Coverage Status"/></a>
|
||||||
<br/>
|
<br/>
|
||||||
<a href="https://maven-badges.herokuapp.com/maven-central/ca.uhn.hapi.fhir/hapi-fhir-base/badge.svg"><img src="https://maven-badges.herokuapp.com/maven-central/ca.uhn.hapi.fhir/hapi-fhir-base/badge.svg" alt="Maven Central"/></a>
|
<a href="https://maven-badges.herokuapp.com/maven-central/ca.uhn.hapi.fhir/hapi-fhir-base/badge.svg"><img src="https://maven-badges.herokuapp.com/maven-central/ca.uhn.hapi.fhir/hapi-fhir-base/badge.svg" alt="Maven Central"/></a>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
Loading…
Reference in New Issue