Fix searching in JPA with _id and _content params

This commit is contained in:
jamesagnew 2017-12-05 07:25:34 -05:00
parent e1c62ef03e
commit d710682fed
10 changed files with 294 additions and 193 deletions

View File

@ -20,18 +20,21 @@ package ca.uhn.fhir.jpa.dao;
* #L% * #L%
*/ */
import static org.apache.commons.lang3.StringUtils.isNotBlank; import ca.uhn.fhir.jpa.entity.ResourceTable;
import ca.uhn.fhir.model.api.IQueryParameterType;
import java.util.*; import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.param.StringParam;
import javax.persistence.*; import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.search.Query; import org.apache.lucene.search.Query;
import org.apache.lucene.search.highlight.*;
import org.apache.lucene.search.highlight.Formatter; import org.apache.lucene.search.highlight.Formatter;
import org.apache.lucene.search.highlight.*;
import org.hibernate.search.jpa.FullTextEntityManager; import org.hibernate.search.jpa.FullTextEntityManager;
import org.hibernate.search.jpa.FullTextQuery; import org.hibernate.search.jpa.FullTextQuery;
import org.hibernate.search.query.dsl.BooleanJunction; import org.hibernate.search.query.dsl.BooleanJunction;
@ -40,15 +43,12 @@ import org.hl7.fhir.dstu3.model.BaseResource;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import com.google.common.collect.Lists; import javax.persistence.EntityManager;
import com.google.common.collect.Sets; import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import java.util.*;
import ca.uhn.fhir.jpa.entity.ResourceTable; import static org.apache.commons.lang3.StringUtils.isNotBlank;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
public class FulltextSearchSvcImpl extends BaseHapiFhirDao<IBaseResource> implements IFulltextSearchSvc { public class FulltextSearchSvcImpl extends BaseHapiFhirDao<IBaseResource> implements IFulltextSearchSvc {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FulltextSearchSvcImpl.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FulltextSearchSvcImpl.class);
@ -62,7 +62,7 @@ public class FulltextSearchSvcImpl extends BaseHapiFhirDao<IBaseResource> implem
public FulltextSearchSvcImpl() { public FulltextSearchSvcImpl() {
super(); super();
} }
private void addTextSearch(QueryBuilder theQueryBuilder, BooleanJunction<?> theBoolean, List<List<? extends IQueryParameterType>> theTerms, String theFieldName, String theFieldNameEdgeNGram, String theFieldNameNGram) { private void addTextSearch(QueryBuilder theQueryBuilder, BooleanJunction<?> theBoolean, List<List<? extends IQueryParameterType>> theTerms, String theFieldName, String theFieldNameEdgeNGram, String theFieldNameNGram) {
if (theTerms == null) { if (theTerms == null) {
return; return;
@ -87,11 +87,12 @@ public class FulltextSearchSvcImpl extends BaseHapiFhirDao<IBaseResource> implem
// .andField(theFieldNameNGram).boostedTo(1.0f) // .andField(theFieldNameNGram).boostedTo(1.0f)
.sentence(terms.iterator().next().toLowerCase()).createQuery(); .sentence(terms.iterator().next().toLowerCase()).createQuery();
//@formatter:on //@formatter:on
theBoolean.must(textQuery); theBoolean.must(textQuery);
} else { } else {
String joinedTerms = StringUtils.join(terms, ' '); String joinedTerms = StringUtils.join(terms, ' ');
theBoolean.must(theQueryBuilder.keyword().onField(theFieldName).matching(joinedTerms).createQuery()); } theBoolean.must(theQueryBuilder.keyword().onField(theFieldName).matching(joinedTerms).createQuery());
}
} }
} }
} }
@ -145,7 +146,7 @@ public class FulltextSearchSvcImpl extends BaseHapiFhirDao<IBaseResource> implem
return pids; return pids;
} }
*/ */
QueryBuilder qb = em.getSearchFactory().buildQueryBuilder().forEntity(ResourceTable.class).get(); QueryBuilder qb = em.getSearchFactory().buildQueryBuilder().forEntity(ResourceTable.class).get();
BooleanJunction<?> bool = qb.bool(); BooleanJunction<?> bool = qb.bool();
@ -183,7 +184,7 @@ public class FulltextSearchSvcImpl extends BaseHapiFhirDao<IBaseResource> implem
List<?> result = jpaQuery.getResultList(); List<?> result = jpaQuery.getResultList();
HashSet<Long> pidsSet = pids != null ? new HashSet<Long>(pids) : null; HashSet<Long> pidsSet = pids != null ? new HashSet<Long>(pids) : null;
ArrayList<Long> retVal = new ArrayList<Long>(); ArrayList<Long> retVal = new ArrayList<Long>();
for (Object object : result) { for (Object object : result) {
Object[] nextArray = (Object[]) object; Object[] nextArray = (Object[]) object;
@ -201,8 +202,16 @@ public class FulltextSearchSvcImpl extends BaseHapiFhirDao<IBaseResource> implem
Long pid = null; Long pid = null;
if (theParams.get(BaseResource.SP_RES_ID) != null) { if (theParams.get(BaseResource.SP_RES_ID) != null) {
StringParam idParm = (StringParam) theParams.get(BaseResource.SP_RES_ID).get(0).get(0); String idParamValue;
pid = BaseHapiFhirDao.translateForcedIdToPid(theResourceName, idParm.getValue(), myForcedIdDao); IQueryParameterType idParam = theParams.get(BaseResource.SP_RES_ID).get(0).get(0);
if (idParam instanceof TokenParam) {
TokenParam idParm = (TokenParam) idParam;
idParamValue = idParm.getValue();
} else {
StringParam idParm = (StringParam) idParam;
idParamValue = idParm.getValue();
}
pid = BaseHapiFhirDao.translateForcedIdToPid(theResourceName, idParamValue, myForcedIdDao);
} }
Long referencingPid = pid; Long referencingPid = pid;
@ -213,6 +222,19 @@ public class FulltextSearchSvcImpl extends BaseHapiFhirDao<IBaseResource> implem
return retVal; return retVal;
} }
@Override
public boolean isDisabled() {
try {
FullTextEntityManager em = org.hibernate.search.jpa.Search.getFullTextEntityManager(myEntityManager);
em.getSearchFactory().buildQueryBuilder().forEntity(ResourceTable.class).get();
} catch (Exception e) {
ourLog.trace("FullText test failed", e);
ourLog.debug("Hibernate Search (Lucene) appears to be disabled on this server, fulltext will be disabled");
return true;
}
return false;
}
@Transactional() @Transactional()
@Override @Override
public List<Long> search(String theResourceName, SearchParameterMap theParams) { public List<Long> search(String theResourceName, SearchParameterMap theParams) {
@ -238,18 +260,18 @@ public class FulltextSearchSvcImpl extends BaseHapiFhirDao<IBaseResource> implem
QueryBuilder qb = em.getSearchFactory().buildQueryBuilder().forEntity(ResourceTable.class).get(); QueryBuilder qb = em.getSearchFactory().buildQueryBuilder().forEntity(ResourceTable.class).get();
Query textQuery = qb Query textQuery = qb
.phrase() .phrase()
.withSlop(2) .withSlop(2)
.onField("myContentText").boostedTo(4.0f) .onField("myContentText").boostedTo(4.0f)
.andField("myContentTextEdgeNGram").boostedTo(2.0f) .andField("myContentTextEdgeNGram").boostedTo(2.0f)
.andField("myContentTextNGram").boostedTo(1.0f) .andField("myContentTextNGram").boostedTo(1.0f)
.andField("myContentTextPhonetic").boostedTo(0.5f) .andField("myContentTextPhonetic").boostedTo(0.5f)
.sentence(theText.toLowerCase()).createQuery(); .sentence(theText.toLowerCase()).createQuery();
Query query = qb.bool() Query query = qb.bool()
.must(qb.keyword().onField("myResourceLinks.myTargetResourcePid").matching(pid).createQuery()) .must(qb.keyword().onField("myResourceLinks.myTargetResourcePid").matching(pid).createQuery())
.must(textQuery) .must(textQuery)
.createQuery(); .createQuery();
FullTextQuery ftq = em.createFullTextQuery(query, ResourceTable.class); FullTextQuery ftq = em.createFullTextQuery(query, ResourceTable.class);
ftq.setProjection("myContentText"); ftq.setProjection("myContentText");
@ -286,7 +308,7 @@ public class FulltextSearchSvcImpl extends BaseHapiFhirDao<IBaseResource> implem
Collections.sort(suggestions); Collections.sort(suggestions);
Set<String> terms = Sets.newHashSet(); Set<String> terms = Sets.newHashSet();
for (Iterator<Suggestion> iter = suggestions.iterator(); iter.hasNext();) { for (Iterator<Suggestion> iter = suggestions.iterator(); iter.hasNext(); ) {
String nextTerm = iter.next().getTerm().toLowerCase(); String nextTerm = iter.next().getTerm().toLowerCase();
if (!terms.add(nextTerm)) { if (!terms.add(nextTerm)) {
iter.remove(); iter.remove();
@ -294,39 +316,11 @@ public class FulltextSearchSvcImpl extends BaseHapiFhirDao<IBaseResource> implem
} }
long delay = System.currentTimeMillis() - start; long delay = System.currentTimeMillis() - start;
ourLog.info("Provided {} suggestions for term {} in {} ms", new Object[] { terms.size(), theText, delay }); ourLog.info("Provided {} suggestions for term {} in {} ms", new Object[]{terms.size(), theText, delay});
return suggestions; return suggestions;
} }
public static class Suggestion implements Comparable<Suggestion> {
public Suggestion(String theTerm, float theScore) {
myTerm = theTerm;
myScore = theScore;
}
public String getTerm() {
return myTerm;
}
public float getScore() {
return myScore;
}
private String myTerm;
private float myScore;
@Override
public int compareTo(Suggestion theO) {
return Float.compare(theO.myScore, myScore);
}
@Override
public String toString() {
return "Suggestion[myTerm=" + myTerm + ", myScore=" + myScore + "]";
}
}
public class MySuggestionFormatter implements Formatter { public class MySuggestionFormatter implements Formatter {
private List<Suggestion> mySuggestions; private List<Suggestion> mySuggestions;
@ -340,26 +334,9 @@ public class FulltextSearchSvcImpl extends BaseHapiFhirDao<IBaseResource> implem
mySuggestions = theSuggestions; mySuggestions = theSuggestions;
} }
public void setFindPhrasesWith() {
myPartialMatchPhrases = new ArrayList<String>();
myPartialMatchScores = new ArrayList<Float>();
for (Suggestion next : mySuggestions) {
myPartialMatchPhrases.add(' ' + next.myTerm);
myPartialMatchScores.add(next.myScore);
}
myPartialMatchPhrases.add(myOriginalSearch);
myPartialMatchScores.add(1.0f);
}
public void setAnalyzer(String theString) {
myAnalyzer = theString;
}
@Override @Override
public String highlightTerm(String theOriginalText, TokenGroup theTokenGroup) { public String highlightTerm(String theOriginalText, TokenGroup theTokenGroup) {
ourLog.debug("{} Found {} with score {}", new Object[] { myAnalyzer, theOriginalText, theTokenGroup.getTotalScore() }); ourLog.debug("{} Found {} with score {}", new Object[]{myAnalyzer, theOriginalText, theTokenGroup.getTotalScore()});
if (theTokenGroup.getTotalScore() > 0) { if (theTokenGroup.getTotalScore() > 0) {
float score = theTokenGroup.getTotalScore(); float score = theTokenGroup.getTotalScore();
if (theOriginalText.equalsIgnoreCase(myOriginalSearch)) { if (theOriginalText.equalsIgnoreCase(myOriginalSearch)) {
@ -379,19 +356,51 @@ public class FulltextSearchSvcImpl extends BaseHapiFhirDao<IBaseResource> implem
return null; return null;
} }
public void setAnalyzer(String theString) {
myAnalyzer = theString;
}
public void setFindPhrasesWith() {
myPartialMatchPhrases = new ArrayList<String>();
myPartialMatchScores = new ArrayList<Float>();
for (Suggestion next : mySuggestions) {
myPartialMatchPhrases.add(' ' + next.myTerm);
myPartialMatchScores.add(next.myScore);
}
myPartialMatchPhrases.add(myOriginalSearch);
myPartialMatchScores.add(1.0f);
}
} }
@Override public static class Suggestion implements Comparable<Suggestion> {
public boolean isDisabled() { private String myTerm;
try { private float myScore;
FullTextEntityManager em = org.hibernate.search.jpa.Search.getFullTextEntityManager(myEntityManager);
em.getSearchFactory().buildQueryBuilder().forEntity(ResourceTable.class).get(); public Suggestion(String theTerm, float theScore) {
} catch (Exception e) { myTerm = theTerm;
ourLog.trace("FullText test failed", e); myScore = theScore;
ourLog.debug("Hibernate Search (Lucene) appears to be disabled on this server, fulltext will be disabled"); }
return true;
@Override
public int compareTo(Suggestion theO) {
return Float.compare(theO.myScore, myScore);
}
public float getScore() {
return myScore;
}
public String getTerm() {
return myTerm;
}
@Override
public String toString() {
return "Suggestion[myTerm=" + myTerm + ", myScore=" + myScore + "]";
} }
return false;
} }
} }

View File

@ -1378,7 +1378,13 @@ public class SearchBuilder implements ISearchBuilder {
throw new InvalidRequestException("Fulltext search is not enabled on this service, can not process parameter: " + Constants.PARAM_CONTENT); throw new InvalidRequestException("Fulltext search is not enabled on this service, can not process parameter: " + Constants.PARAM_CONTENT);
} }
} }
List<Long> pids = myFulltextSearchSvc.everything(myResourceName, myParams);
List<Long> pids;
if (myParams.getEverythingMode() != null) {
pids = myFulltextSearchSvc.everything(myResourceName, myParams);
} else {
pids = myFulltextSearchSvc.search(myResourceName, myParams);
}
if (pids.isEmpty()) { if (pids.isEmpty()) {
// Will never match // Will never match
pids = Collections.singletonList(-1L); pids = Collections.singletonList(-1L);
@ -1576,7 +1582,9 @@ public class SearchBuilder implements ISearchBuilder {
cq.where(from.get("myId").in(pids)); cq.where(from.get("myId").in(pids));
TypedQuery<ResourceTable> q = entityManager.createQuery(cq); TypedQuery<ResourceTable> q = entityManager.createQuery(cq);
for (ResourceTable next : q.getResultList()) { List<ResourceTable> resultList = q.getResultList();
for (ResourceTable next : resultList) {
Class<? extends IBaseResource> resourceType = context.getResourceDefinition(next.getResourceType()).getImplementingClass(); Class<? extends IBaseResource> resourceType = context.getResourceDefinition(next.getResourceType()).getImplementingClass();
IBaseResource resource = theDao.toResource(resourceType, next, theForHistoryOperation); IBaseResource resource = theDao.toResource(resourceType, next, theForHistoryOperation);
Integer index = position.get(next.getId()); Integer index = position.get(next.getId());
@ -1615,7 +1623,7 @@ public class SearchBuilder implements ISearchBuilder {
// when running asserts // when running asserts
assert new HashSet<>(theIncludePids).size() == theIncludePids.size() : "PID list contains duplicates: " + theIncludePids; assert new HashSet<>(theIncludePids).size() == theIncludePids.size() : "PID list contains duplicates: " + theIncludePids;
Map<Long, Integer> position = new HashMap<Long, Integer>(); Map<Long, Integer> position = new HashMap<>();
for (Long next : theIncludePids) { for (Long next : theIncludePids) {
position.put(next, theResourceListToPopulate.size()); position.put(next, theResourceListToPopulate.size());
theResourceListToPopulate.add(null); theResourceListToPopulate.add(null);

View File

@ -263,7 +263,7 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
} }
// Execute the query and make sure we return distinct results // Execute the query and make sure we return distinct results
List<IBaseResource> resources = new ArrayList<IBaseResource>(); List<IBaseResource> resources = new ArrayList<>();
sb.loadResourcesByPid(pidsSubList, resources, includedPids, false, myEntityManager, myContext, myDao); sb.loadResourcesByPid(pidsSubList, resources, includedPids, false, myEntityManager, myContext, myDao);
return resources; return resources;

View File

@ -23,6 +23,8 @@ package ca.uhn.fhir.jpa.search;
import java.util.List; import java.util.List;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionCallback;
@ -36,7 +38,7 @@ import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl.SearchTask;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
public class PersistedJpaSearchFirstPageBundleProvider extends PersistedJpaBundleProvider { public class PersistedJpaSearchFirstPageBundleProvider extends PersistedJpaBundleProvider {
private static final Logger ourLog = LoggerFactory.getLogger(PersistedJpaSearchFirstPageBundleProvider.class);
private SearchTask mySearchTask; private SearchTask mySearchTask;
private ISearchBuilder mySearchBuilder; private ISearchBuilder mySearchBuilder;
private Search mySearch; private Search mySearch;
@ -54,20 +56,31 @@ public class PersistedJpaSearchFirstPageBundleProvider extends PersistedJpaBundl
@Override @Override
public List<IBaseResource> getResources(int theFromIndex, int theToIndex) { public List<IBaseResource> getResources(int theFromIndex, int theToIndex) {
SearchCoordinatorSvcImpl.verifySearchHasntFailedOrThrowInternalErrorException(mySearch); SearchCoordinatorSvcImpl.verifySearchHasntFailedOrThrowInternalErrorException(mySearch);
ourLog.trace("Fetching search resource PIDs");
final List<Long> pids = mySearchTask.getResourcePids(theFromIndex, theToIndex); final List<Long> pids = mySearchTask.getResourcePids(theFromIndex, theToIndex);
ourLog.trace("Done fetching search resource PIDs");
TransactionTemplate txTemplate = new TransactionTemplate(myTxManager); TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
txTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRED); txTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRED);
return txTemplate.execute(new TransactionCallback<List<IBaseResource>>() { List<IBaseResource> retVal = txTemplate.execute(new TransactionCallback<List<IBaseResource>>() {
@Override @Override
public List<IBaseResource> doInTransaction(TransactionStatus theStatus) { public List<IBaseResource> doInTransaction(TransactionStatus theStatus) {
return toResourceList(mySearchBuilder, pids); return toResourceList(mySearchBuilder, pids);
}}); }
});
ourLog.trace("Loaded resources to return");
return retVal;
} }
@Override @Override
public Integer size() { public Integer size() {
ourLog.trace("Waiting for initial sync");
Integer size = mySearchTask.awaitInitialSync(); Integer size = mySearchTask.awaitInitialSync();
ourLog.trace("Finished waiting for local sync");
SearchCoordinatorSvcImpl.verifySearchHasntFailedOrThrowInternalErrorException(mySearch); SearchCoordinatorSvcImpl.verifySearchHasntFailedOrThrowInternalErrorException(mySearch);
if (size != null) { if (size != null) {
return size; return size;

View File

@ -117,8 +117,13 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
if (myNeverUseLocalSearchForUnitTests == false) { if (myNeverUseLocalSearchForUnitTests == false) {
SearchTask task = myIdToSearchTask.get(theUuid); SearchTask task = myIdToSearchTask.get(theUuid);
if (task != null) { if (task != null) {
ourLog.trace("Local search found");
return task.getResourcePids(theFrom, theTo); return task.getResourcePids(theFrom, theTo);
} else {
ourLog.trace("No local search found");
} }
} else {
ourLog.trace("Forced not using local search");
} }
TransactionTemplate txTemplate = new TransactionTemplate(myManagedTxManager); TransactionTemplate txTemplate = new TransactionTemplate(myManagedTxManager);

View File

@ -1,11 +1,9 @@
package ca.uhn.fhir.jpa.config; package ca.uhn.fhir.jpa.config;
import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory;
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
import ca.uhn.fhir.validation.ResultSeverityEnum; import ca.uhn.fhir.validation.ResultSeverityEnum;
import net.ttddyy.dsproxy.listener.ThreadQueryCountHolder; import net.ttddyy.dsproxy.listener.ThreadQueryCountHolder;
import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
import org.apache.commons.dbcp2.BasicDataSource; import org.apache.commons.dbcp2.BasicDataSource;
import org.hibernate.jpa.HibernatePersistenceProvider; import org.hibernate.jpa.HibernatePersistenceProvider;
@ -30,6 +28,17 @@ import static org.junit.Assert.*;
public class TestR4Config extends BaseJavaConfigR4 { public class TestR4Config extends BaseJavaConfigR4 {
static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TestR4Config.class); static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TestR4Config.class);
private static int ourMaxThreads;
static {
/*
* We use a randomized number of maximum threads in order to try
* and catch any potential deadlocks caused by database connection
* starvation
*/
ourMaxThreads = (int) (Math.random() * 6.0) + 1;
}
private Exception myLastStackTrace; private Exception myLastStackTrace;
@Bean() @Bean()
@ -90,13 +99,7 @@ public class TestR4Config extends BaseJavaConfigR4 {
retVal.setUsername(""); retVal.setUsername("");
retVal.setPassword(""); retVal.setPassword("");
/* retVal.setMaxTotal(ourMaxThreads);
* We use a randomized number of maximum threads in order to try
* and catch any potential deadlocks caused by database connection
* starvation
*/
int maxThreads = (int) (Math.random() * 6.0) + 1;
retVal.setMaxTotal(maxThreads);
DataSource dataSource = ProxyDataSourceBuilder DataSource dataSource = ProxyDataSourceBuilder
.create(retVal) .create(retVal)
@ -155,4 +158,8 @@ public class TestR4Config extends BaseJavaConfigR4 {
return retVal; return retVal;
} }
public static int getMaxThreads() {
return ourMaxThreads;
}
} }

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.jpa.provider.r4; package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.jpa.config.TestR4Config;
import ca.uhn.fhir.jpa.dao.DaoConfig; import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.entity.Search;
import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl; import ca.uhn.fhir.jpa.search.SearchCoordinatorSvcImpl;
@ -176,7 +177,6 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
return ids; return ids;
} }
// Y
@Test @Test
public void testBundleCreate() throws Exception { public void testBundleCreate() throws Exception {
IGenericClient client = myClient; IGenericClient client = myClient;
@ -1569,24 +1569,56 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
} }
@Test @Test
public void testGetResourceCountsOperation() throws Exception { public void testFulltextEverythingWithIdAndContent() throws IOException {
String methodName = "testMetaOperations"; Patient p = new Patient();
p.setId("FOO");
p.addName().setFamily("FAMILY");
myClient.update().resource(p).execute();
Patient pt = new Patient(); p = new Patient();
pt.addName().setFamily(methodName); p.setId("BAR");
myClient.create().resource(pt).execute().getId().toUnqualifiedVersionless(); p.addName().setFamily("HELLO");
myClient.update().resource(p).execute();
HttpGet get = new HttpGet(ourServerBase + "/$get-resource-counts"); Observation o = new Observation();
CloseableHttpResponse response = ourHttpClient.execute(get); o.setId("BAZ");
try { o.getSubject().setReference("Patient/FOO");
assertEquals(200, response.getStatusLine().getStatusCode()); o.getCode().setText("GOODBYE");
String output = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); myClient.update().resource(o).execute();
IOUtils.closeQuietly(response.getEntity().getContent());
ourLog.info(output); List<String> ids = searchAndReturnUnqualifiedVersionlessIdValues(ourServerBase + "/Patient/FOO/$everything?_content=White");
assertThat(output, containsString("<parameter><name value=\"Patient\"/><valueInteger value=\"")); assertThat(ids, contains("Patient/FOO"));
} finally {
response.close(); ids = searchAndReturnUnqualifiedVersionlessIdValues(ourServerBase + "/Patient/FOO/$everything?_content=HELLO");
} assertThat(ids, contains("Patient/FOO"));
ids = searchAndReturnUnqualifiedVersionlessIdValues(ourServerBase + "/Patient/FOO/$everything?_content=GOODBYE");
assertThat(ids, containsInAnyOrder("Patient/FOO", "Observation/BAZ"));
}
@Test
public void testFulltextSearchWithIdAndContent() throws IOException {
Patient p = new Patient();
p.setId("FOO");
p.addName().setFamily("FAMILY");
myClient.update().resource(p).execute();
p = new Patient();
p.setId("BAR");
p.addName().setFamily("HELLO");
myClient.update().resource(p).execute();
Observation o = new Observation();
o.setId("BAZ");
o.getSubject().setReference("Patient/FOO");
o.getCode().setText("GOODBYE");
myClient.update().resource(o).execute();
List<String> ids = searchAndReturnUnqualifiedVersionlessIdValues(ourServerBase + "/Patient?_id=FOO&_content=family");
assertThat(ids, contains("Patient/FOO"));
ids = searchAndReturnUnqualifiedVersionlessIdValues(ourServerBase + "/Patient?_id=FOO&_content=HELLO");
assertThat(ids, empty());
} }
// private void delete(String theResourceType, String theParamName, String theParamValue) { // private void delete(String theResourceType, String theParamName, String theParamValue) {
@ -1614,6 +1646,27 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
// } // }
// } // }
@Test
public void testGetResourceCountsOperation() throws Exception {
String methodName = "testMetaOperations";
Patient pt = new Patient();
pt.addName().setFamily(methodName);
myClient.create().resource(pt).execute().getId().toUnqualifiedVersionless();
HttpGet get = new HttpGet(ourServerBase + "/$get-resource-counts");
CloseableHttpResponse response = ourHttpClient.execute(get);
try {
assertEquals(200, response.getStatusLine().getStatusCode());
String output = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
IOUtils.closeQuietly(response.getEntity().getContent());
ourLog.info(output);
assertThat(output, containsString("<parameter><name value=\"Patient\"/><valueInteger value=\""));
} finally {
response.close();
}
}
@Test @Test
public void testHasParameter() throws Exception { public void testHasParameter() throws Exception {
IIdType pid0; IIdType pid0;
@ -2049,6 +2102,54 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
} }
@Test
public void testParseAndEncodeExtensionWithValueWithExtension() throws IOException {
String input = "<Patient xmlns=\"http://hl7.org/fhir\">\n" +
" <extension url=\"https://purl.org/elab/fhir/network/StructureDefinition/1/BirthWeight\">\n" +
" <valueDecimal>\n" +
" <extension url=\"http://www.hl7.org/fhir/extension-data-absent-reason.html\">\n" +
" <valueCoding>\n" +
" <system value=\"http://hl7.org/fhir/ValueSet/birthweight\"/>\n" +
" <code value=\"Underweight\"/>\n" +
" <userSelected value=\"false\"/>\n" +
" </valueCoding>\n" +
" </extension>\n" +
" </valueDecimal>\n" +
" </extension>\n" +
" <identifier>\n" +
" <system value=\"https://purl.org/elab/fhir/network/StructureDefinition/1/EuroPrevallStudySubjects\"/>\n" +
" <value value=\"1\"/>\n" +
" </identifier>\n" +
" <gender value=\"female\"/>\n" +
"</Patient>";
HttpPost post = new HttpPost(ourServerBase + "/Patient");
post.setEntity(new StringEntity(input, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
CloseableHttpResponse response = ourHttpClient.execute(post);
IdType id;
try {
assertEquals(201, response.getStatusLine().getStatusCode());
String newIdString = response.getFirstHeader(Constants.HEADER_LOCATION_LC).getValue();
assertThat(newIdString, startsWith(ourServerBase + "/Patient/"));
id = new IdType(newIdString);
} finally {
response.close();
}
HttpGet get = new HttpGet(ourServerBase + "/Patient/" + id.getIdPart() + "?_pretty=true");
response = ourHttpClient.execute(get);
try {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp);
assertEquals(200, response.getStatusLine().getStatusCode());
assertThat(resp, containsString("Underweight"));
} finally {
IOUtils.closeQuietly(response.getEntity().getContent());
response.close();
}
}
@Test @Test
public void testPatchUsingJsonPatch() throws Exception { public void testPatchUsingJsonPatch() throws Exception {
String methodName = "testPatchUsingJsonPatch"; String methodName = "testPatchUsingJsonPatch";
@ -3205,14 +3306,19 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
ourLog.info(myCapturingInterceptor.getLastResponse().getAllHeaders().toString()); ourLog.info(myCapturingInterceptor.getLastResponse().getAllHeaders().toString());
assertThat(myCapturingInterceptor.getLastResponse().getHeaders(Constants.HEADER_X_CACHE), Matchers.<String>empty()); assertThat(myCapturingInterceptor.getLastResponse().getHeaders(Constants.HEADER_X_CACHE), Matchers.<String>empty());
assertThat(myCapturingInterceptor.getLastResponse().getHeaders(Constants.HEADER_X_CACHE.toLowerCase()),Matchers.<String>empty()); assertThat(myCapturingInterceptor.getLastResponse().getHeaders(Constants.HEADER_X_CACHE.toLowerCase()), Matchers.<String>empty());
String msg = "Total is " + found.getTotalElement().getValue() + " and took " + sw.getMillis() + " millis"; String msg = "Total is " + found.getTotalElement().getValue() + " and took " + sw.getMillis() + " millis";
// If this fails under load, try increasing the throttle above // When we've only got one DB connection available, we are forced to wait for the
assertEquals(msg,null, found.getTotalElement().getValue()); // search to finish before returning
assertEquals(msg, 1, found.getEntry().size()); if (TestR4Config.getMaxThreads() > 1) {
assertThat(msg, sw.getMillis(), lessThan(1000L)); assertEquals(null, found.getTotalElement().getValue());
assertEquals(1, found.getEntry().size());
assertThat(sw.getMillis(), lessThan(1000L));
} else {
assertThat(sw.getMillis(), greaterThan(1000L));
}
} }
@ -3239,9 +3345,9 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
assertThat(sw.getMillis(), not(lessThan(1000L))); assertThat(sw.getMillis(), not(lessThan(1000L)));
// If this fails under load, try increasing the throttle above
assertEquals(10, found.getTotalElement().getValue().intValue()); assertEquals(10, found.getTotalElement().getValue().intValue());
assertEquals(1, found.getEntry().size()); assertEquals(1, found.getEntry().size());
} }
@Test @Test
@ -3266,12 +3372,17 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
.execute(); .execute();
assertThat(myCapturingInterceptor.getLastResponse().getHeaders(Constants.HEADER_X_CACHE), Matchers.<String>empty()); assertThat(myCapturingInterceptor.getLastResponse().getHeaders(Constants.HEADER_X_CACHE), Matchers.<String>empty());
assertThat(myCapturingInterceptor.getLastResponse().getHeaders(Constants.HEADER_X_CACHE.toLowerCase()),Matchers.<String>empty()); assertThat(myCapturingInterceptor.getLastResponse().getHeaders(Constants.HEADER_X_CACHE.toLowerCase()), Matchers.<String>empty());
// If this fails under load, try increasing the throttle above // WHen we've only got one DB connection available, we are forced to wait for the
assertEquals(null, found.getTotalElement().getValue()); // search to finish before returning
assertEquals(1, found.getEntry().size()); if (TestR4Config.getMaxThreads() > 1) {
assertThat(sw.getMillis(), lessThan(1500L)); assertEquals(null, found.getTotalElement().getValue());
assertEquals(1, found.getEntry().size());
assertThat(sw.getMillis(), lessThan(1500L));
} else {
assertThat(sw.getMillis(), greaterThan(1500L));
}
} }
@Test @Test
@ -4353,56 +4464,6 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
} }
} }
@Test
public void testParseAndEncodeExtensionWithValueWithExtension() throws IOException {
String input = "<Patient xmlns=\"http://hl7.org/fhir\">\n" +
" <extension url=\"https://purl.org/elab/fhir/network/StructureDefinition/1/BirthWeight\">\n" +
" <valueDecimal>\n" +
" <extension url=\"http://www.hl7.org/fhir/extension-data-absent-reason.html\">\n" +
" <valueCoding>\n" +
" <system value=\"http://hl7.org/fhir/ValueSet/birthweight\"/>\n" +
" <code value=\"Underweight\"/>\n" +
" <userSelected value=\"false\"/>\n" +
" </valueCoding>\n" +
" </extension>\n" +
" </valueDecimal>\n" +
" </extension>\n" +
" <identifier>\n" +
" <system value=\"https://purl.org/elab/fhir/network/StructureDefinition/1/EuroPrevallStudySubjects\"/>\n" +
" <value value=\"1\"/>\n" +
" </identifier>\n" +
" <gender value=\"female\"/>\n" +
"</Patient>";
HttpPost post = new HttpPost(ourServerBase + "/Patient");
post.setEntity(new StringEntity(input, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
CloseableHttpResponse response = ourHttpClient.execute(post);
IdType id;
try {
assertEquals(201, response.getStatusLine().getStatusCode());
String newIdString = response.getFirstHeader(Constants.HEADER_LOCATION_LC).getValue();
assertThat(newIdString, startsWith(ourServerBase + "/Patient/"));
id = new IdType(newIdString);
} finally {
response.close();
}
HttpGet get = new HttpGet(ourServerBase + "/Patient/" + id.getIdPart() + "?_pretty=true");
response = ourHttpClient.execute(get);
try {
String resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(resp);
assertEquals(200, response.getStatusLine().getStatusCode());
assertThat(resp, containsString("Underweight"));
} finally {
IOUtils.closeQuietly(response.getEntity().getContent());
response.close();
}
}
@Test @Test
public void testValueSetExpandOperation() throws IOException { public void testValueSetExpandOperation() throws IOException {
CodeSystem cs = myFhirCtx.newXmlParser().parseResource(CodeSystem.class, new InputStreamReader(ResourceProviderR4Test.class.getResourceAsStream("/extensional-case-3-cs.xml"))); CodeSystem cs = myFhirCtx.newXmlParser().parseResource(CodeSystem.class, new InputStreamReader(ResourceProviderR4Test.class.getResourceAsStream("/extensional-case-3-cs.xml")));

View File

@ -178,7 +178,7 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
if (theRequest.getRequestType() == RequestTypeEnum.GET) { if (theRequest.getRequestType() == RequestTypeEnum.GET) {
boolean first = true; boolean first = true;
Map<String, String[]> parameters = theRequest.getParameters(); Map<String, String[]> parameters = theRequest.getParameters();
for (String nextParamName : new TreeSet<String>(parameters.keySet())) { for (String nextParamName : new TreeSet<>(parameters.keySet())) {
for (String nextParamValue : parameters.get(nextParamName)) { for (String nextParamValue : parameters.get(nextParamName)) {
if (first) { if (first) {
b.append('?'); b.append('?');

View File

@ -77,7 +77,6 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
* Check for parameter combinations and names that are invalid * Check for parameter combinations and names that are invalid
*/ */
List<IParameter> parameters = getParameters(); List<IParameter> parameters = getParameters();
// List<SearchParameter> searchParameters = new ArrayList<SearchParameter>();
for (int i = 0; i < parameters.size(); i++) { for (int i = 0; i < parameters.size(); i++) {
IParameter next = parameters.get(i); IParameter next = parameters.get(i);
if (!(next instanceof SearchParameter)) { if (!(next instanceof SearchParameter)) {
@ -93,12 +92,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
} }
} }
// searchParameters.add(sp);
} }
// for (int i = 0; i < searchParameters.size(); i++) {
// SearchParameter next = searchParameters.get(i);
// // next.
// }
/* /*
* Only compartment searching methods may have an ID parameter * Only compartment searching methods may have an ID parameter

View File

@ -17,6 +17,10 @@
RFC 3986. This affects client calls, as well as URLs generated by RFC 3986. This affects client calls, as well as URLs generated by
the server (e.g. REST HOOK calls). Thanks to James Daily for reporting! the server (e.g. REST HOOK calls). Thanks to James Daily for reporting!
</action> </action>
<action type="fix">
Searching in JPA server using a combination of _content and _id parameters
failed. Thanks to Jeff Weyer for reporting!
</action>
</release> </release>
<release version="3.1.0" date="2017-11-23"> <release version="3.1.0" date="2017-11-23">
<action type="add"> <action type="add">